From cc093be67bffa68c770ff3088d6cbc7da0e6575d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B1=BBC=E8=AF=AD=E8=A8=80=E6=98=AF=E4=B8=80=E5=AE=B6?= <51352133+F-Unction@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:19:37 +0800 Subject: [PATCH 001/434] update org.reflections 0.9.12->0.10.2 to avoid `[INFO] Unknown command` (#148) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 97477456a..6dc53ca61 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.1' - implementation group: 'org.reflections', name: 'reflections', version: '0.9.12' + implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' implementation group: 'dev.morphia.morphia', name: 'core', version: '1.6.1' From 317bf5c78b28417a729701784293eb8be3814975 Mon Sep 17 00:00:00 2001 From: Magix Date: Tue, 26 Apr 2022 21:37:28 -0400 Subject: [PATCH 002/434] Update Grasscutter.java why do i have to fix stable --- src/main/java/emu/grasscutter/Grasscutter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index abcdc3557..ad1107e8e 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -34,7 +34,7 @@ public final class Grasscutter { private static DispatchServer dispatchServer; private static GameServer gameServer; - public static final Reflections reflector = new Reflections(); + public static final Reflections reflector = new Reflections("emu.grasscutter"); static { // Declare logback configuration. From 1ee3d284c871e12e6e8d0de5e8eedce404bd76c1 Mon Sep 17 00:00:00 2001 From: memetrollsXD Date: Sat, 30 Apr 2022 23:01:28 +0200 Subject: [PATCH 003/434] Delete bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ---------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea782..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. From 6e007a4be0cf88d5983a0f6f3ae3432b80ec1ba5 Mon Sep 17 00:00:00 2001 From: memetrollsXD Date: Sat, 30 Apr 2022 23:01:35 +0200 Subject: [PATCH 004/434] Delete feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7d6..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. From 85c7f5b54b0d521b20a482ad493560f28ce75d03 Mon Sep 17 00:00:00 2001 From: NostalgiaCyan Date: Sun, 1 May 2022 05:04:30 +0800 Subject: [PATCH 005/434] Update README_zh-CN.md (#382) --- README_zh-CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README_zh-CN.md b/README_zh-CN.md index 6216cf3eb..2558d52fc 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -102,7 +102,7 @@ chmod +x gradlew 你可能需要在终端中运行 `java -jar grasscutter.jar -handbook` 它将会创建一个 `GM Handbook.txt` 以方便您查阅物品ID等 -在每个玩家的朋友列表中都有一个名为“服务器”的虚拟用户,你可以通过发送消息来使用命令。命令也适用于其他聊天室,例如私人/团队聊天。 +在每个玩家的朋友列表中都有一个名为“Server”的虚拟用户,你可以通过发送消息来使用命令。命令也适用于其他聊天室,例如私人/团队聊天。 要在游戏中使用命令,需要添加 `/` 或 `!` 前缀,如 `/pos` | 命令 | 用法 | 权限节点 | 可用性 | 注释 | 别名 | @@ -132,7 +132,7 @@ chmod +x gradlew | say | say \ <消息> | server.sendmessage | 均可使用 | 作为服务器发送消息给玩家 | `sendservmsg` `sendservermessage` `sendmessage` | | setfetterlevel | setfetterlevel <好感等级> | player.setfetterlevel | 仅客户端 | 设置当前角色的好感等级 | `setfetterlvl` `setfriendship` | | setstats | setstats <属性> <数值> | player.setstats | 仅客户端 | 直接修改当前角色的面板 | stats | -| setworldlevel | setworldlevel <世界等级> | player.setworldlevel | 仅客户端 | 设置世界等级(重新登陆即可生效) | setworldlvl | +| setworldlevel | setworldlevel <世界等级> | player.setworldlevel | 仅客户端 | 设置世界等级(重新登录即可生效) | setworldlvl | | spawn | spanw <实体ID\|实体名称> [等级] [数量] | server.spawn | 仅客户端 | 在你周围生成实体 | | | stop | stop | server.stop | 均可使用 | 停止服务器 | | | talent | talent <天赋ID> <等级> | player.settalent | 仅客户端 | 设置当前角色的天赋等级 | | From 777c0e5cb4108bda13ba6b50de68a0ef737bf1fa Mon Sep 17 00:00:00 2001 From: Magix Date: Sat, 30 Apr 2022 17:40:02 -0400 Subject: [PATCH 006/434] Delete bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ---------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea782..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. From a263f125467a34752ff1f04850edb0e785f903cb Mon Sep 17 00:00:00 2001 From: Magix Date: Sat, 30 Apr 2022 17:40:04 -0400 Subject: [PATCH 007/434] Delete feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7d6..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. From 2e4dcbcfbe8de0ccc9ca795dd6891350594e8a52 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 30 Apr 2022 18:02:19 -0400 Subject: [PATCH 008/434] Update to `1.0.3-dev` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 05a51c990..787b98f9d 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'tech.xigam' -version = '1.0.2-dev' +version = '1.0.3-dev' sourceCompatibility = 17 targetCompatibility = 17 From 14c4673d488665c989e368db12b827eb65535453 Mon Sep 17 00:00:00 2001 From: Muhammad Eko Prasetyo Date: Sun, 1 May 2022 05:43:50 +0700 Subject: [PATCH 009/434] Fix DispatchHttpJson handler and add setHttpServer because Express doesn't support removing defined route. (#388) --- .../server/dispatch/DispatchHttpJsonHandler.java | 2 +- .../emu/grasscutter/server/dispatch/DispatchServer.java | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index 81908f9ef..80e582b5f 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -36,7 +36,7 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING") && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { Grasscutter.getLogger().info(String.format("[Dispatch] Client %s %s request: ", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING") ? "(MISSING)" : "")); - res.send(response.getBytes()); } + res.send(response); } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 8b41d2a6f..27167931d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -50,6 +50,12 @@ public final class DispatchServer { return httpServer; } + public void setHttpServer(Express httpServer) { + this.httpServer.stop(); + this.httpServer = httpServer; + this.httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); + } + public Gson getGsonFactory() { return gson; } @@ -271,12 +277,12 @@ public final class DispatchServer { }); // Login + httpServer.post("/hk4e_global/mdk/shield/api/login", (req, res) -> { // Get post data LoginAccountRequestJson requestData = null; try { String body = req.ctx().body(); - Grasscutter.getLogger().info(body); requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class); } catch (Exception ignored) { } From d47c9e1f980ca6e85e9600f8554c5380fc50031e Mon Sep 17 00:00:00 2001 From: muhammadeko Date: Sun, 1 May 2022 06:36:21 +0700 Subject: [PATCH 010/434] Add getter for innerclass RegionData --- .../emu/grasscutter/server/dispatch/DispatchServer.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 27167931d..bc43ad5c8 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -524,5 +524,13 @@ public final class DispatchServer { this.parsedRegionQuery = prq; this.Base64 = b64; } + + public QueryCurrRegionHttpRsp getParsedRegionQuery() { + return parsedRegionQuery; + } + + public String getBase64() { + return Base64; + } } } From 8830da8bc1fa59ae7dbf9e7a26ccb0f9ee711ecb Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 30 Apr 2022 20:37:43 -0400 Subject: [PATCH 011/434] Fix SLF4j issue --- build.gradle | 5 +++++ ...ava-express-1.1.3.jar => java-express.jar} | Bin 15213555 -> 15089450 bytes 2 files changed, 5 insertions(+) rename lib/{java-express-1.1.3.jar => java-express.jar} (88%) diff --git a/build.gradle b/build.gradle index 787b98f9d..9488639e9 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,7 @@ dependencies { implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.9' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.9' + implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' @@ -81,6 +82,10 @@ dependencies { protobuf files('proto/') } +configurations.all { + exclude group: 'org.slf4j', module: 'slf4j' +} + application { // Define the main class for the application mainClassName = 'emu.grasscutter.Grasscutter' diff --git a/lib/java-express-1.1.3.jar b/lib/java-express.jar similarity index 88% rename from lib/java-express-1.1.3.jar rename to lib/java-express.jar index f8d56fd5d273aaee50e1fc3d8bf9a2d49ce3b34b..4df9a40810ea058b140751bd95bc462368a09558 100644 GIT binary patch delta 576294 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 >O&b*4Q-lfg$a9OsY9+p4s7vd5Pd73xAq9HT=ox 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 z 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 B^b-I8 delta 565003 zcmZU)Q*hvI@Fg546Wg|J+qOBe?fjC7ZEIrNwlU$vwr$M5``vu|-|eb?Zu(UBx#){i zb^59N&VYUx`wa~!%Yj3{f`GulfKc1#se=3$VE_C1FMj-&3KHrf3^Iz6OyD5@6W9oV zUE}=+!1*uH|4*QTh@y<7gqk{|g5-n3^pw0D1LHh`90Tp_^q(eG<|Ve>qx~bW|GydQ z|1%cmzi#GUPA=xIuK&-}|4JZW$(a!N1phZ5>MJWVHr)TJ2_PVJ$psMT>92>|xWJWw zLGp&A%O+p`6|-kYbh0{yqgkijRi}rDj%q5JbvlN>p=M$;Z{1IdBNufsV`))1Q(}-! zj4zE;^}GkNYaS+VSq;Mbq_)qP50VPEmuV32{b1sjs#Lp<>t=Fpwl5ZeEdc?6*neMr z|K4vD%Unwyu(oFZ@CpHS53@{seZXmV*ZkWITfp=ODAL$h@E+S-^iY0uCh&(jt>LGun-3K+B!uziz&_n73_1rj&_?ekBRVTVcZQYk<&i{Yps? z&&{)Fz&%a)pKyI5(!aSs)5Zf96-2K){WtPYrk+ShgnpyL(dr>Tj1MoL$#Bhuz7C6~ z09OW+9MHdGq}x9N@hMBi_Phtp@=D3@eDH2j`HjhNnto0O2j)hQ9H^kbGNNSvG`pd+ zrXOPIHzpfc#9=-^cQ6uCj|D71Kha);H=wo?Gj8z7h%(4ao5)jHkP+<5_X;E)29=DW zyk;f&WpI!LFkcU3d`NGNJcvLh^|*G|5{*}gu21~0rvn4gSHYGZR&$xtrTk#J~kg$ZGe|Tqn57g2L3~UBcO&B6kc|w6KBqovdLN`wiRE|mvu!6 zW!?G)B%Nzw*mDzq$p8t4V%(Pe1`;zw%kfwlE!kRjw{J zhJOs}8A6PYe%$x0dNJA=1t+MyP!$PiiWqG2Jl>`X(Rtu5`(%V0W-CGu_)`f24d$WKMQ);J}EzTP>gIdg}pBC^g7k4@}jSI?`udozqy0s;l z8EzdB)G(OqRRO*Nao=XgUbaueA=dXChTx+{`Do(C*Ni|>#5LvJ`}@_%d_`?u6v?8b zZ31mNFS$A)26p;XyGb#jEXZ=}`Z5+X#TD%`Q_ru2l27SC9Ed<>f*cyNTry!<(nB@o zaK`lwh#8x6q9M=v=+%A7R^x!I2;)OHB85)~pWwVXTp-CZ<1&HaL5wrKDMO-Rn8$O~ zK5rTYP!bQyIVhh985=ZN@8rzLg^E45RjFvA&=DIkS#&aAJesV~J@ zOKnu_4nWmB9rfF>JqJ}>O9k5BW=e{%!lCz44_^0c`g-q5B>b@U_#&n2;w477_uxNS z=kC&(T949M&tLk0JEa;*MYr(srfOqf&$eUW-IZK&duWfe9a|gC#k5JQ! zznFS*sRXBIzfw_~VUl<*A&jtP_>Fq$O(wUu3j^pon-g8&MY2z%Szq({K*2-4XV3rA zV469pCiqK+CZk2QhADWdvhZaZvJy!4!gLRtU^J-_`ol&*+LcY@yh`p{lV)pQnCIE$ z0tzL6Vl~$cO`q_(-3IY~pGs17j|evi5k_l!>Buo;*k@41sr=S5&O1*SsJ^rzzx_q@ zOTG+Cmwp;l>MAgtT)JFZm3Ag$yrTf09!F04AEe{RA-Nc8XjwEe;#P2&F)I5d`rS? zpghMdPI6>y-%Wr~t8emDbGSX4R;Iq;>PRKgv?eDQxf2^73JDqs5mbAVTMYx#rR{q^ znI$7VEMhh}L7`J8FaJP)%sVJ#g0Yxa3%2wY#$phc-?*A~#x2UyBH89s6iPlMO(4`0 zIN!Wd-k+a7GkDKW4s!mUt3s1FCN%L9_#cE0ncg<|xTj^x0|Hi(=zfPEi=V_)piA>5> zz$;QatCAbxALZ4b|~9Se2zfLen%Ohl>LIxbrFkoAc4+?v>UbS__eRE|sSTtv-dD5bqI{ zL4wrTvP99sY^JqIOp#I;nQfKE!B-{?Fyjw%vIcj)A{PQly5<*}k)?qt^-;-P2>4hn zJ5ov>q{-->_2qoE`{3~I`{{x*V{ysX2>1Y;xEJvNJ@JuNoqo4hgM7u>oSnwyfkMtc zx!Jvbc7oqvDX9$iss?A&k-CD`{Banm_T)hFaAE;rMG|z8jx?Dj3*SCV{vdJZzbiA_ zrXI`uQw9z!#=NFaP|k|C|KdM&;sh5qEPckt|R6~XpBNq0bV zR%_C*sa*0Aqn%!1FGEasP1qM9^|YixZ_R_0gd^SV zr}jsxJyBg6i&ZUUrhH-=iXnwpbFsh|wSDPr4Xt;Rx(th`dK4T-!C*F~yF&Y|4d=ZJ zrpq5QlvCVh=b-!)FsRoZ-uHNau&HOs=$t&cGz?+xb$L{@BMG;(3mDNA5K3tfF=_ick*kyw*wwNWwzNQ1vFJnF ze3XiVPxLx6;N%zY598H}@zbiWW!Z+6cHglk<+lt`Q#-EDC5=>)_g(LHzKQ0K?*aT*Qh zh%Q1{N|P)Kion;%urn&k!4QCk7`aWJi%5`i>)*)J$IR?2&nrxgD8fjfj7G6Y z39^QH0)q_he#*vq5nJe5iGQkr>_fv$SX0UyB_w7dh{>2Ae?Ihdy>FOz>zr(dD-zN* zAvxlPAGWdGGx06950iWe43*v%l{c1+Y?Bb!=HZW-)yR-c*b-Uz*M!?nq*re4iQ9`5 z{qRnLh8p3l7sx3kSPrEu#1u&)EDMeVWr6e^Yp^J3*(dHaNDRi1UiJC?`7e9vtB1~% zdiupFRMoNZj0PBtvZ695+nuT3ZftuFvT<=1L&Sg7`MM5XJ{|z62Wn5er5(rA5-=6L zV>qb>V0y8-FU@dA54Wr4IwFX%-igDe(f++!GdG^RGE|}}jFKMRrlF_~#sV(QHf`I5E@DkCqkV&MthJtQ z*pEuW7wwIoa5R8(@TmIP`pD8;$ULIFj>e_pZ+i?dLhm+9FL(M8&%2iyCx<74L zs-(PCmf_#Di3XZsyPt^l*{dgMdpKbB&dMF3ERt9l<_J8V3L+!`eH5tcbtkd!OI`V~ z94XuUc#A{oMzYC&evQv9sR_+LmJgrQe4b&E=8lvn*9`wCa?V_6pWKizOj1!>tcIX2 z-M)P9@#PVhLaJqMK^Yoq{5)J8y&y(^Dv$ld{_^F4dr*(_As&g5-k}34#235GoKkjC zEAmlv{s+{!NeR@0Rn2r#i!O=wz#hy9Tk?V~uMUdSi?!SEvc~8s$#Hn)5F!A}GB%QX zrMzdt6lKqB-J+;84;tPXn3vf)h8%{^6Q|Tw5H zs7COS<*p>AH<*ZhUBew7RG>@J3e_9=tWfLcgZiWt?n_SogGLFPCQDzZTSBYfY@PfM zjS5*VAu>yQBmuT^2$fd(;>;qM6I~D!mc_4n)4<)W9%8=N`Sg9%mfklf9X2@%ogB0x zxelFBo6>O-?v#KLzH9m2;l>&b8?s5gR(6Yl#{u`DK!DO{?WA z8P?CXIBxTh<8F<}fT&RE+r?`uX^u99=CT`R;Q5tCs$3E|1xq6d@`x9KV$~x{f7M0f zb3Q!rgQ227-Mr)wWsT@v+*5c7JA2hcpe8aJ!M#qBc13GYmy1iEM-QuD^W-1zZH++07M4))358Oe4FzN{%q`;Uvc@VYgl+{0 z9t0gxMV(~RigwP(;vIkJ(n_n=&`SoHDW@b;BswkQhz+&g1<7L#0AZ%?_3hePkY9T1 z4`XSF3R=+on!&rlP7?jmZt;s8FiidFca!q%R7u0fBK1&6<&!hC@XEqiXGnhwgu;8E z?mqs04L6*KHOxocr)|@Lgq1R2^V&5JbO|9#u0wrHAnVs&&;6`G&7-`G>kxy=F3P z!yW2g++dFXm6zGp$uF=E>fa?;<-qmHF63TC)`{!^p3JGv!5N2rHg?fL5K ztATA)_whBm8+^XmwH3GzKMY)pTlxVN8%vsbUN2FrY}~EWFH%#{X~nP)lPRaI6(iLZ zwvze8&ga?>z+;06a$$5&yYoDPw@`(L`aHzgyrO&4D!}C3m9pGM0Ku$47(gRZ$o%;Zm3jZ8>2k}w*Pu^Hn?CG#;dHjgN;wkI& zF6-C^49{GAQ{`>Cr`7@(()iBM_c%1y&l?3%PYJ3mcuqB`kDm5|^gEc8)J|!nio~0B z>(i%MssB^74c6E91xn7aT8*gbX_5AZBn1WCG6B%@N;48M7JM#$?GjaF9L&`Vaxj+M zF&>gkw9@m}Ih1GH^;D5mPc}3C5wtq2M;_{dV|VkyY>9Lgo@0a;m`gd*^Etd|I&+aD zAHtu@JL_-m%llacL6#m0UdlhJ^;D}h&@40W4>kSk-$n8r3t=zqo2IbZ8cWtU^Ufpf zoAL&FEqNU@(x$7Zy*0ErF4^Cd=Wcz|nbx&r?)QyCfGbc9#!l966={c3AtTFynFK$8 zu8q2F$GCU!>fr~=^H5SheE|M+v6kky;^y{4b zUkmyd5?4CyRSjpAFu##C3cJyutu9eBY$dumV;jcdIhs*RTVrzLxp+xdE&|yF9EswG zX#uq^gZ^{3?DYa`hn#IlFK3QUZHyjJVdz{dnoiJ?BFYzHn|hdaOBvX zbtGi}MwjE&7q-vkAW!W_7vOF;AS|f|%5Q|Ao$Y$3D|?`4-`>jIsva@$Mmx+8dCZa0 zw_C%huk`t=b)A{p(jHrxbp@{gKDcet$=}+p0{-h(zhoY1O)H(z^wKojpyL&2*?Xw+ z{~^9T>&WKFaCEJjT~s?|k899C(Jl(*`dQ;@FIhsDUDc>FL%?Ih-O2~2``7nk`)!)_ zzHtOkIqf{&)u-c!1l^_kUr9D)8>3phPRqOoODx1u{buQ5#)dhfaJz>ChVJcxS|_SY zJd`Q>ZoWPP>ZdD3uW5r+LTg#zeP5Z4J}$LAqpwubR~I{WbiNLKCd}%qQcmuUL&7z< zzj_*T{_)L?8a$@dm+%<%JWmo(EfwA%yP@nj?TIn|D{lWVsF_03sn3lOS+gxYEX1e} z*v5Tr$d~K>CrFoTW){;XF~_X_ZnhqdVX{dbS-NT#dv(49k1&taY0(1}2*%=SoQLs- zNkYfbNuN=cVdt@2#MErx&C|1RC)x~&E?YYpG6~Ky1>F{l_51YY*}-0CyRq3ZWha7I zWe%EpdGZG%)Omxm_(r>1yCK=n9qEgT)LTN0SIep`A?nD@@_4{79Y{34(FnE(M97(CQZkn~d8k^Qdn*F(22@XU*W1a4|X53oD69uj`-c6o@EGvYw

6TetutsAVu9|k?6o`T{3^(j+&zPJxYqNaKUShEW*z#Z3~i3FpdDTXIrHU_UgNs; zVb2z%Yf$nzNa5W+rWMQLZH`nyTIn0DKJC6{ck&#s4a$DBBfOWGQ7Q$TNu?VzhPL`c7(;>I&N^jLh zc7Jx$lw{PGn*U|kET_n9x?;r4@m8EGgE&)X6KNzrUec~EeU72Eo#4(}0FlS`QVmjU zCp}Z!AY<$7lH4V%?kN(3>nEoM;2B<4xzBf>3|~`?$WRPbbEK|!%lUWC1rH0F=N64Km*#%Q>rhTra#4F zNV>F^8vi98*W+jQCqc!oV)$$Y2)*tr-OC#wo53%&m`gl_MlI!_!+}x_-r@OKc6@aY zAWt=txLImOtAeG^|I7ZxlzN$xsMb>bzl>nnfHV7Mp!WsJ#2{4aMBLJm;~3TKPYI^y zou57(4gFN3fSu+o-JqmTxEu=Cx8cv1v1(1VT1oBrF*s1w@^EvkdrEC+SZnEIeQt^>{K1@A^hPgVk@QM?-r^P53ROn7I}Od%>kHWah-k zSkaCBq^Ec{`7(C=kfUg;g%a6uTxyooN{TDF21wJnH3C_*;xv1PuDcN_Jz4|$dKrPu zo@nVz-U;{@*E4U3edY!YMQC^?pp%D7UFve(bPH7}bHTvhMh9y6doo?>FAn^l3S>gY zFO}B&YtXjmdqkzcfDN)B5JEk=AVGSWw_=M;Ci|L0n8`{4Q)E>(;`GH?sO;LZRBFkK zv_jjmx-F>Lm@8mQ8CH9_v^^hPz#IWmodjnieR^!8^q_cdmQCGo zyOe1&AnRph)@JE0P8Kus62j+zQW3@r5d$dEfm@`1R1oAH%x@75P^dC>*d|@d^F7k9%Fs5I_IcVnmvc47Q>kucepxsbo&=+Oso!r;OK!1K|EqYO2jC z&1L_uE^E;A3H&*e9qPEP-S#??q3eH1*Kzar zfk$R+1u9=|Ad5CtH~0+ivhHR3Bv7n9iEO;NUicdzi)VT-FAL1uX8DW$I^guzBVBl=I7Py z;R{wcLUSLOL;6+}&>&uGz2=7RVONz_D>LnDOp~L7Pw^x=ZVmNjYeO9sIbzSi;B!2} zAaU^=mOC|7l_QP~4mRMO%k(o>G}t>6Cu?XJ4hvRz?2-xWy50B#>@95zIf6voD$}5m zBZAZUra}_EPrU_frwRqTj=UzRuYUD$0gDUZPhI7*Srr{@Ku?YiUcs;LP2SfvWeU4c z;Mj(Q-G+PLI5D_APw;NQ=RNFrOB8lSY6y0^jqpY?CAg19?WyK1(iUBY+)8woK^K~U zH-znVI=Ai~>fS!Ajc2F!EnA0mYu$OJcX(!V-T9xtn}%XOp&#_NSDdY&Pp{OGzt>G<%yVCgBY&&Odvd*rmaVjV><3#=motMU*@@ z39_OaCd|67Z5J4_(nRCRiPibTx<Kpm=a_kV}@VkylOS}GDYL?>an|H=)N8?jT zTW`slx7}zxT2eG=3*Jk+V=(5`yNDUtCAr>{8$D8X`Mi7dwfLZ&|Fp>8&^o;5{3-|I zrU!c4UjL~3ZR(|*wPRbDKgj9azu|wMzJ9g&)ZOnzu=r5Q%r#ky8gFfnx|tp)wa@n^~+*Jacdv zcKy@GP_5%oGu09 z3bT>H!jC48?E+Y+GWiuCYfSQUD|Dzwc5yecR}6MP{z4Fo`(5yoWDPX~;&v7H9zD5I z+)~7{9z8nOI<_saeu~%?3_CAZ@m?Q>wY{U?0UoT*Fm=QxaxM)>)m7Yl;dQN8;i)c$ zt#`@$Y7e-r6l)Ud4^{!wA<@^s@5vj*y~HIm!%o!pm{u3|vX>_HDlU!`7u)P(lde}* zTmXYDREd&}g&-QCQEgiB07*D*c=in`u!;b-D>i0A(df$SxZ6apw74q|+cn|q9tMj+ zK|FY)#~v3C7ZFT?DgBYsG~zddz+#W3 zqIf<}SJz}lwKlNj&>z;a|MOynYgHMwSKm}aTLIy%OcHg`g=IHz+sx?~#cg;(FApy$ zVk`11>eS(7@o{kl#Au9seZG7gl&(^rDP3b?R{&;Q-xMnxA!4vOpB+x-g9gsb3Bzev zfJ+0FKNjETsXZThs8eg0wT3uE#6}%32ip8=aa%EK!>FbAK0jj;M3#qT7h@cas?$eW zvYDbYV6d}>4yT%;f6mGju&H9N*jT&T_rK0k-XHMD5o@aOCT{L-uP^(>!<0Pt{fYr> z2%fsswW@(_GL%gdVXUd;&r0U7VGgVjWwe7#oyOLcDP$B_qhIbe`|t>%ewHISmS!YS zvm=Q&A$Cs_*<*1D!LVg#X6!!?l0i5D#KsRKZ;OpG#9_01FFzn`p@K|Z(FIcyb_>>L zouf?Qfj-7y`vE?EpF?CXa3;`6rtoM1`*?Gk#IFyWcW+y{E?YN{X%eg3m_P|Gvc94p zXV|#d5VtbcC>{V}Dk#XUv9GL(=k3O^-E(^Y)?1l^`wjd@C#TcX`r+d`WeVG}PW;Fk zSokZm=L&Edg8ML;MkU|DU1DE@WUKhWHFd>NTEMJT$;}+ynz;(BCNaDVZFmtPv*v$L zvD4gbBpO|a$84t?D*S- zMWcC3nZgUYY=|V09S5`lwuu6k9&RP;$+vgyW3~eK8A_nQK z`AvOhjy(vHEmQ>Mryn{iGRPVYS*N*H6^Q+US87augv%_%_88*R-LmRoK%^?LMF7*J zW4p){UR`GGt*F=gJA186U9Q?m{bg!oc`;^ra5tI4V`YqNtN4$mXE7!d)#&Q(vd^OY z*~eK`@6#H?1WdxSOWkF@w5*rRPn`V>(IO$l1U)7Z?Uq9si_uCJ^a&-;{1&-2(> z=AxQ`PnO|MZq5EOSFt(9_5W$i_W$&hPa3^-mNoo-n(r@*5v9Tq3^5M{$)DOMp8ww8E{8`yf+ z6S`tcKTeY=>}D}o566+;_CrG!Yl|>a{RqR`cc$#1s4EOxShB=+JFxxPYgcA9B!?u* zPVunzueSM4M$5ZZ2z-|_K|ku!xOp;#btRKuwe8^g7Jwatm1px0{V=8-7RVINQ!&`r zZH^K1&}bNL;nang(-+E;#P%4Xz_`!SP#}K$IRtq+bk|au!aG)sds$%d4J}AGTB|k| z)S>NH%A8eegZ&-KExK&5Z9jV{y!|Eov1^G&j~zUD)DZU(V}_3P7&HpE>;!)tThvrt zUA>$7m^SKf9#pTpwWa&RybGshPEl{uMty#Hb-#y`)5svpEtos-M@``{-LZBtuF4)D(fDj zuAe++z_C4gy1Jcoz0z~j)tuIw_f|DG?>##tUwx5Fu!=-aRUz_r2u9?6X+_CXu-pk! z{m*sED(|t%-dLwpYcJJu+LR(a(ZjT*(ay|SlmB_qapqLBRjpE2cKa2tvd1)K`;SN8 ztAkFo$gQxNU=Xa^_J-D!*Evc{%9|hT)4a20!4`EFmkzgd*R?A=*nMTVwM(Pc8@f6v zSzWzort6q-;>N`rE~n=Fczesz!|9q|S$4C`30=sq-Pza9Cu!QYDJ+`TZo$m1=^o+g zt4CVAX?TA6U`>Nv|5$ou_dR5${b1r59edy8tdVacmw66XotU(1`twWgREHfZ>_0g? z@t0@ZvbOgF12(Pid^_)b<-ZFmPU_yUiQN3-`~6RaW461l{H`RfSOk&e^=|eG;y~bC zv|Y!3){6#8l6}QP(x>!U-JtoBQ)+NnG*EM%L9PQld6NII{ zd)242b=)tmem*{VRr4n%zh3?LL6!H;d+oH-V*a^%T{dSJx*24e^xHK1{K_q@w%R@E z^ys-y*wfrK`~A*t$~xB~^7~YCYJ3 zT572mnsZQf($3rdX*BIq&be_>8E2=a7OaRGe$e@4eEICA3AQob*#`nmJeL-`MkwX( zCe50U9kTd`#jqxix_7)Yc9SykJO8QAnD$y~Azk{Uw7xhkvc;BDs&Ts%3?17&?{W)ZJK(Qofm*n~+==u#P>w82 z=gd~MeuXL5`|F>)2OVp?3^{0m#Ytz-bS9DAGl$m_`IA8_ zGJbd_U)$!0UvI3JKIUJv(c76i^vHA;Z&_s@ZyjDk7dJu%Z)OqVh2+h_MOl}EnFeRg~(?UEC&ye~(;^y2!f@cxBD<4F<`cvi;vDlA+>Q4Q8vE+0U<}-D*$&!j83!~b4&q-r9P(6%S%%Bc5#Xc)kj6@kC)x7Ox zw%f7>s|5mu`Ee3r>FAg>MvU?p%x&o8SxJ!NOq|e$SW+8g9p&XV#`D>3O=xO_wm>JT zL6$nLZj@y$rc4=`(PrAil&}~LY)qSaX8DS%PxkTEy@g)v=DIot0ttFnnib3cysW5X z`ZeZdRRw|5*ydUH#Som)*cNJNQ~?CLrlS!F^~|!P%UfhAlru8WgqUxO*s+R(&>j~- zbmUPgCTu~gtUS@4GGx5#0jnk8=qxeH$*Ip!24v4>>|CjK%9>WYH&bg~2@;7oKpB?B zUn|cjIg_Uc@=56KT0UvF)5ySetlasLG%rWyL^w;o9}3Fk)e6U_Rf{a$Hy& zmQ});&-l>>#RmZ`A`W#;aU~!cC2`?dx&OgcOhi^=ZpVl7TW&}f$7D5Y$aP`uC$o-8s3!4h zDs+K@?kUUvVCvZHoQAeT;s$Z|b+X20deJiW3dmtilF=56Ey1eBoh!Ge!**qT=e4)d z2eZ)S7;0Z#aeC7Od$SISCm_VZx&Kd=v;BxslZH(AtIdmNE(b=T?6-$Mi77R;Cg~+v zx@6mVw5f6^E1GwNMs1{LZ{s$Fbpi6+t$@|PP;Nu>)3WZ0&&spDT< zZ}kLe;7$lRUYqK?%W~nTW!JsWE>vO^zH9M#W4hvV))(GRjG8t&;~ms?Jn{fPvs_5Q zCBz0#s;h{Enn?uf&}|#OKzHHJN%{Ghr;iFW%qq=K`s@Z znKoC-juDBlKK|SP=Z;fw>@Tq^<1P@PuT-+Lc?Vx;Hv63#$n+G_=}z>UZni>!H6znp z>EQY&us89z>X}VzG^9y}Wk9Uc46>i{wuyL5IrlHjPC7y?p~?CrF9Wg6X`}4-ykqGf zd?>#hRNZj?4c3;vHP8OR+vB0CqudVzvABjaf2-_l9^tX@%fb)v?3wkcgH5)#c*PQ9 zTk`X}H@meABPIkQjBUEbGy4yF>*W2_wH&%;jj|?|_Yhzae%YVJygQ?ob0n(&2-s3Q zK3hwT0H#iKv&wbpuIAYzd9OUoQ~4*$oDGsY9oi#2JDTTSayfb_yQQd!wp^uzmVZTL z_v5u`O52JzjWLRm#uP!{Fn;t#on7bH2+n4!v7Gy3BeHd+-Pts(OwCxQ(KYZ7t2T${=! zXAcu!8)7ioysh^RVWxZIO)y<3jR0G=FguN(>FRsW7PG5P(u^sbK_=fhyLQbUkH&Pi zs>|k)17n9KuFIas&t(7d%KhF@x%)Wg{u>psCA%H3Zkqpe>U#(c6`RVHnDIkEOqIUa znVrtJ+wjNi8SK1vNKHg4V!xzhm++%waI-992ZlLJP*ORMw#Tz6KhzByjp}0=hOF5R zxxE)Cp1oRbMMF+zTZeIWVVd{X)Po(cGrEy<377YptXgnImF`!%lHGxW_U(R6HX#UA z#Iu~v*e5Cq@Cdd0Ps}!X=A>vnSoQPg0-ytx}D{oA_ZF7{`aE&?TwA6TsvctUQ z#3%sE3cwoBje~POS^q~vaHFn&HKa+@3u2*QxW=1NNIICKMb6(q6cKd;KD)`BoKd`{ zO&zhk?qz!ffh~n0_>H%wh6{4C_ytkN`-_Ih!6f!qB}S%(jC6g{Bp&^LHr4{5yAgE^0QfZ?3D*INUTNk1FuE5#~7rKL}UB$K-SfqMl>!fXA<99N`-@2J;n~Sks*R}a_o7;h*hDg zt$}E&N`X+g9iL6w9)nuNuu#KQQ~vX|c{xe{6=g8vU^i*nxFLT98PUciX{jJT1E>6N z=2Y=*uiK!bsf7-9!|OeW!)e@|97leTmRXETWr!ekidZ6?|EQGwbu&8@Zlf+={RZAb zAQyW-CsnLi6zx~&qny9v!G85xJ@|$GRcgyAB)-Uj{VvbZqh}uFXz&g{mUbFi57@hS z`2%K!Ff0YuouB_5rnhWd0r%0!4vkEC(Xe@_PkX3jKlfgbVx1Uvj zUS-$_7K4J*D!wllLOkE6qD?%HCZr-D9yN0KUbzYJ(#bu;&Gk`z7o1hbptcrvJ8Rf8 zC@MFit#xx%`7vI$M$*F$;Ytf%xuF!gGNuTI@|Z!cgLuZ4F?nHmQqXrWIjo^PFcz>0 zuDROO*)CUyUw6&!dTs{07zEFM3TX_6rNFM@eaXScz^}d%52YPHHFUW$dENK4aoh&?ks=j>~sJUcT$%J4noT}q`Bw$G5k1f<9&Uc6I68- zVdT0DrmVb4uAz7#2J5)yt!W?F-eelc_9w@i*GsG_*SrQEs!CBM9$lOg@wVC|JDlNJgmG11x>cGySNjU$%Gx_t^ zbPUV%GFYu>g{+W@s+e|i>|wzTHiSGd|-xBpvh(LQ^EZ_ zo63B)SDP0Uea>-C)aYkqd0<;!-N{y+TfUyHX>*{P?W`piB4i<*~0lWTjGgWK3xp4CE)k6a(m4^8HsJA&2<>=mcd}YGmsJ*)T-uNticcXjC)1;xT z*Ln3b9Aka!ePGWMagle78#MW#`LXu}ov0rRR?hw>(fPhaCtBrt=o#Oc89l9v{ZpRo zT;l&kHtOMtn6txVCqj;Wa9`;b?pEMd;C?>E_*=Sxm_C)xnAGL^XpNW?IJG%0lAfEV zbE*pOQAE>mdCheG@^j*C6 zrM7)yFIC4Hd=_fx#tkgzYg!p7v7qSG59r1dFA}pdxS;?d9ilOHo1RYlB}Rt~TV$;C z&u%1jF50lC5bG2B7^HfAO49aealAZsio#JG@km!A120MHf5u+khk8F=YNZjmsnnD#Eqg_XN!LF|m z&POd$8h2S>SCjl?Jcqtc`z~5AhV5k=_kvwSV~z7)m+|i6Z$1R#I3_(1Nthic(j=nz zmo3=XqQ?%mHrDPqIy{A(JFX8V@&M)d%^E=I!sFLWrK zepEJG6cVG~*3!>x9e$C9w;@l+6y7mr!7 z(w~=PNg`SHQGLOLQ_u0s`AT@*6u+!Jl*2yS_w;jPMYj$xe(dnj%9nUqNi|cJU8_+H zRrvhAHCyo?P@-=$WeWB2?Bl6+YRX6O(L;$C?vKp1a#0)hV|J365x5Bgbilw8 zg=!>5A!&70TUG_(tWGZsDoGa8F6?7Z)jRds{j@UtS&^H=O3dljp(Q%JM%qhrn^b~_ zLrOG%Sc$2aMPXy&Jwod~OOdi#a!(Q(TVhQr4{I9|k0KnEXgZ>#gj2q;fr*{FL5@~+ zY17z}cu{bS)Yk**0jvsZHf{nKTk@W7$JmQzozX9TVluf5JH z5KvkWv!uj|9P6X4L0e5MnIrax!7?%%_^@Je9VIqEav0H{T$017_N^d?DXjQ>42-Fh zAQ}?%9rcq9rj#fYpEDtDZ}HrirKPliY$z9^ZCr`DICEK}hw}U;-a*el<0&`BTj_U9 zlf~0Z?ukdL7_HtLLkwA+Aj`IS%4sDhmMCOn7^qFtyQ;PT*$e%&p)Kc>c!|Wp(>owYm}}mW#Ra5@iWHA=4B;U zYQ@h`m)>63wE|HL3M_n2Z0nd!Pu;?dS*#*-bj_6KUi{>*Q"b~KZim4LWbhzY8tt)0^@PES< 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švq(45uqFAdYoV)fzL&K%(V;F!B>F`$|7z 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

p)|E_%^``2rq7jo3t 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 From 6fb0b5494f6eacf7c8f5b6cba05b29bbd5379846 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 30 Apr 2022 20:38:18 -0400 Subject: [PATCH 012/434] Implement a handler for logging routes --- .../server/dispatch/ClientLogHandler.java | 19 +++++++++++++++++++ .../server/dispatch/DispatchServer.java | 8 ++++---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java diff --git a/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java b/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java new file mode 100644 index 000000000..4b57826ea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.dispatch; + +import express.http.HttpContextHandler; +import express.http.Request; +import express.http.Response; + +import java.io.IOException; + +/** + * Used for processing crash dumps & logs generated by the game. + * Logs are in JSON, and are sent to the server for logging. + */ +public final class ClientLogHandler implements HttpContextHandler { + @Override + public void handle(Request request, Response response) throws IOException { + // TODO: Figure out how to dump request body and log to file. + response.send("{\"code\":0}"); + } +} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index bc43ad5c8..bf236bd88 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -35,11 +35,11 @@ public final class DispatchServer { private final String defaultServerName = "os_usa"; public String regionListBase64; - public HashMap regions; + public Map regions; private Express httpServer; public DispatchServer() { - this.regions = new HashMap(); + this.regions = new HashMap<>(); this.gson = new GsonBuilder().create(); this.loadQueries(); @@ -475,9 +475,9 @@ public final class DispatchServer { // Logging servers // overseauspider.yuanshen.com - httpServer.all("/log", new DispatchHttpJsonHandler("{\"code\":0}")); + httpServer.all("/log", new ClientLogHandler()); // log-upload-os.mihoyo.com - httpServer.all("/crash/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + httpServer.all("/crash/dataUpload", new ClientLogHandler()); httpServer.get("/gacha", (req, res) -> res.send("Gacha")); From b703a325443f7dfe14e9f8130c5e95cd083efaf9 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 30 Apr 2022 21:52:30 -0400 Subject: [PATCH 013/434] QoL changes --- src/main/java/emu/grasscutter/game/drop/DropManager.java | 3 +-- src/main/java/emu/grasscutter/game/shop/ShopManager.java | 3 +-- .../grasscutter/server/dispatch/DispatchHttpJsonHandler.java | 4 ---- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/drop/DropManager.java b/src/main/java/emu/grasscutter/game/drop/DropManager.java index 0e8807cfe..e304d37b5 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropManager.java +++ b/src/main/java/emu/grasscutter/game/drop/DropManager.java @@ -53,8 +53,7 @@ public class DropManager { Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); } } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Grasscutter.getLogger().error("Unable to load drop data.", e); } } private void addDropEntity(DropData dd, Scene dropScene, ItemData itemData, Position pos, int num, Player target) { diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index 96dd932a1..bad68afff 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -85,8 +85,7 @@ public class ShopManager { }); } } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Grasscutter.getLogger().error("Unable to load shop data.", e); } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index 80e582b5f..5305c4a85 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -1,12 +1,8 @@ package emu.grasscutter.server.dispatch; import java.io.IOException; -import java.io.OutputStream; import java.util.Arrays; -import java.util.Collections; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; import emu.grasscutter.Grasscutter; import express.http.HttpContextHandler; import express.http.Request; From dd0f8f8d496541f773aaf1b49c6e8e62f028d36d Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sat, 30 Apr 2022 19:34:50 -0700 Subject: [PATCH 014/434] Fix constellations that give an extra charge to skills Fix #228 --- proto/AvatarSkillMaxChargeCountNotify.proto | 9 ++++ .../emu/grasscutter/data/ResourceLoader.java | 17 ++----- .../data/custom/OpenConfigEntry.java | 49 +++++++++++++++++-- .../emu/grasscutter/game/avatar/Avatar.java | 34 ++++++++++++- .../game/avatar/AvatarStorage.java | 2 +- .../game/managers/InventoryManager.java | 20 ++++++-- ...PacketAvatarSkillMaxChargeCountNotify.java | 21 ++++++++ 7 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 proto/AvatarSkillMaxChargeCountNotify.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillMaxChargeCountNotify.java diff --git a/proto/AvatarSkillMaxChargeCountNotify.proto b/proto/AvatarSkillMaxChargeCountNotify.proto new file mode 100644 index 000000000..abbd17d2d --- /dev/null +++ b/proto/AvatarSkillMaxChargeCountNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message AvatarSkillMaxChargeCountNotify { + uint64 avatar_guid = 1; + uint32 skill_id = 2; + uint32 max_charge_count = 3; +} diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 180080e51..5478052d8 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -307,18 +307,7 @@ public class ResourceLoader { } for (Entry e : config.entrySet()) { - List abilityList = new ArrayList<>(); - int extraTalentIndex = 0; - - for (OpenConfigData entry : e.getValue()) { - if (entry.$type.contains("AddAbility")) { - abilityList.add(entry.abilityName); - } else if (entry.talentIndex > 0) { - extraTalentIndex = entry.talentIndex; - } - } - - OpenConfigEntry entry = new OpenConfigEntry(e.getKey(), abilityList, extraTalentIndex); + OpenConfigEntry entry = new OpenConfigEntry(e.getKey(), e.getValue()); map.put(entry.getName(), entry); } } @@ -354,9 +343,11 @@ public class ResourceLoader { public OpenConfigData[] data; } - private static class OpenConfigData { + public static class OpenConfigData { public String $type; public String abilityName; public int talentIndex; + public int skillID; + public int pointDelta; } } diff --git a/src/main/java/emu/grasscutter/data/custom/OpenConfigEntry.java b/src/main/java/emu/grasscutter/data/custom/OpenConfigEntry.java index d01467637..8ff646fa9 100644 --- a/src/main/java/emu/grasscutter/data/custom/OpenConfigEntry.java +++ b/src/main/java/emu/grasscutter/data/custom/OpenConfigEntry.java @@ -1,18 +1,39 @@ package emu.grasscutter.data.custom; +import java.util.ArrayList; import java.util.List; +import emu.grasscutter.data.ResourceLoader.OpenConfigData; + public class OpenConfigEntry { private String name; private String[] addAbilities; private int extraTalentIndex; - - public OpenConfigEntry(String name, List abilityList, int extraTalentIndex) { + private SkillPointModifier[] skillPointModifiers; + + public OpenConfigEntry(String name, OpenConfigData[] data) { this.name = name; - this.extraTalentIndex = extraTalentIndex; + + List abilityList = new ArrayList<>(); + List modList = new ArrayList<>(); + + for (OpenConfigData entry : data) { + if (entry.$type.contains("AddAbility")) { + abilityList.add(entry.abilityName); + } else if (entry.talentIndex > 0) { + this.extraTalentIndex = entry.talentIndex; + } else if (entry.$type.contains("ModifySkillPoint")) { + modList.add(new SkillPointModifier(entry.skillID, entry.pointDelta)); + } + } + if (abilityList.size() > 0) { this.addAbilities = abilityList.toArray(new String[0]); } + + if (modList.size() > 0) { + this.skillPointModifiers = modList.toArray(new SkillPointModifier[0]); + } } public String getName() { @@ -26,4 +47,26 @@ public class OpenConfigEntry { public int getExtraTalentIndex() { return extraTalentIndex; } + + public SkillPointModifier[] getSkillPointModifiers() { + return skillPointModifiers; + } + + public static class SkillPointModifier { + private int skillId; + private int delta; + + public SkillPointModifier(int skillId, int delta) { + this.skillId = skillId; + this.delta = delta; + } + + public int getSkillId() { + return skillId; + } + + public int getDelta() { + return delta; + } + } } diff --git a/src/main/java/emu/grasscutter/game/avatar/Avatar.java b/src/main/java/emu/grasscutter/game/avatar/Avatar.java index cf5446ab7..de66e8671 100644 --- a/src/main/java/emu/grasscutter/game/avatar/Avatar.java +++ b/src/main/java/emu/grasscutter/game/avatar/Avatar.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.bson.types.ObjectId; @@ -18,6 +19,7 @@ import dev.morphia.annotations.Transient; import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.FightPropData; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.OpenConfigEntry.SkillPointModifier; import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.AvatarPromoteData; import emu.grasscutter.data.def.AvatarSkillData; @@ -46,6 +48,7 @@ import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.net.proto.AvatarFetterInfoOuterClass.AvatarFetterInfo; import emu.grasscutter.net.proto.AvatarInfoOuterClass.AvatarInfo; +import emu.grasscutter.net.proto.AvatarSkillInfoOuterClass.AvatarSkillInfo; import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData; import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass; import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass.ShowAvatarInfo; @@ -83,6 +86,7 @@ public class Avatar { private List fetters; private Map skillLevelMap; // Talent levels + private Map skillExtraChargeMap; // Charges private Map proudSkillBonusMap; // Talent bonus levels (from const) private int skillDepotId; private int coreProudSkillLevel; // Constellation level @@ -123,6 +127,7 @@ public class Avatar { this.flyCloak = 140001; this.skillLevelMap = new HashMap<>(); + this.skillExtraChargeMap = new HashMap<>(); this.talentIdList = new HashSet<>(); this.proudSkillList = new HashSet<>(); @@ -283,6 +288,13 @@ public class Avatar { public Map getSkillLevelMap() { return skillLevelMap; } + + public Map getSkillExtraChargeMap() { + if (skillExtraChargeMap == null) { + skillExtraChargeMap = new HashMap<>(); + } + return skillExtraChargeMap; + } public Map getProudSkillBonusMap() { return proudSkillBonusMap; @@ -676,9 +688,10 @@ public class Avatar { } } - public void recalcProudSkillBonusMap() { + public void recalcConstellations() { // Clear first this.getProudSkillBonusMap().clear(); + this.getSkillExtraChargeMap().clear(); // Sanity checks if (getData() == null || getData().getSkillDepot() == null) { @@ -699,6 +712,21 @@ public class Avatar { continue; } + // Check if we can add charges to a skill + if (entry.getSkillPointModifiers() != null) { + for (SkillPointModifier mod : entry.getSkillPointModifiers()) { + AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId()); + + if (skillData == null) continue; + + int charges = skillData.getMaxChargeNum() + mod.getDelta(); + + this.getSkillExtraChargeMap().put(mod.getSkillId(), charges); + } + continue; + } + + // Check if a skill can be boosted by +3 levels int skillId = 0; if (entry.getExtraTalentIndex() == 2 && this.getData().getSkillDepot().getSkills().size() >= 2) { @@ -788,6 +816,10 @@ public class Avatar { .setWearingFlycloakId(this.getFlyCloak()) .setCostumeId(this.getCostume()); + for (Entry entry : this.getSkillExtraChargeMap().entrySet()) { + avatarInfo.putSkillMap(entry.getKey(), AvatarSkillInfo.newBuilder().setMaxChargeCount(entry.getValue()).build()); + } + for (GameItem item : this.getEquips().values()) { avatarInfo.addEquipGuidList(item.getGuid()); } diff --git a/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java b/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java index 99ccda173..2486e36ab 100644 --- a/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java +++ b/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java @@ -148,7 +148,7 @@ public class AvatarStorage implements Iterable { avatar.setOwner(getPlayer()); // Force recalc of const boosted skills - avatar.recalcProudSkillBonusMap(); + avatar.recalcConstellations(); // Add to avatar storage this.avatars.put(avatar.getAvatarId(), avatar); diff --git a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java index 1e028c38c..2a9629b73 100644 --- a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java +++ b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java @@ -8,6 +8,7 @@ import java.util.stream.Collectors; import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.OpenConfigEntry.SkillPointModifier; import emu.grasscutter.data.def.AvatarPromoteData; import emu.grasscutter.data.def.AvatarSkillData; import emu.grasscutter.data.def.AvatarSkillDepotData; @@ -835,9 +836,22 @@ public class InventoryManager { // Proud skill bonus map (Extra skills) OpenConfigEntry entry = GameData.getOpenConfigEntries().get(talentData.getOpenConfig()); - if (entry != null && entry.getExtraTalentIndex() > 0) { - avatar.recalcProudSkillBonusMap(); - player.sendPacket(new PacketProudSkillExtraLevelNotify(avatar, entry.getExtraTalentIndex())); + if (entry != null) { + if (entry.getExtraTalentIndex() > 0) { + // Check if new constellation adds +3 to a skill level + avatar.recalcConstellations(); + // Packet + player.sendPacket(new PacketProudSkillExtraLevelNotify(avatar, entry.getExtraTalentIndex())); + } else if (entry.getSkillPointModifiers() != null) { + // Check if new constellation adds skill charges + avatar.recalcConstellations(); + // Packet + for (SkillPointModifier mod : entry.getSkillPointModifiers()) { + player.sendPacket( + new PacketAvatarSkillMaxChargeCountNotify(avatar, mod.getSkillId(), avatar.getSkillExtraChargeMap().getOrDefault(mod.getSkillId(), 0)) + ); + } + } } // Recalc + save avatar diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillMaxChargeCountNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillMaxChargeCountNotify.java new file mode 100644 index 000000000..696cb2027 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillMaxChargeCountNotify.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarSkillMaxChargeCountNotifyOuterClass.AvatarSkillMaxChargeCountNotify; + +public class PacketAvatarSkillMaxChargeCountNotify extends BasePacket { + + public PacketAvatarSkillMaxChargeCountNotify(Avatar avatar, int skillId, int maxCharges) { + super(PacketOpcodes.AvatarSkillMaxChargeCountNotify); + + AvatarSkillMaxChargeCountNotify proto = AvatarSkillMaxChargeCountNotify.newBuilder() + .setAvatarGuid(avatar.getGuid()) + .setSkillId(skillId) + .setMaxChargeCount(maxCharges) + .build(); + + this.setData(proto); + } +} From 582d91227cf14d729954e8c78a919e265913cdef Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sat, 30 Apr 2022 21:16:50 -0700 Subject: [PATCH 015/434] Fix crash when login * Prevent email to be `null` to avoid crash in certain client setup. --- .../grasscutter/server/dispatch/DispatchServer.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index bf236bd88..c8902e3f6 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -381,6 +381,9 @@ public final class DispatchServer { responseData.data.account.uid = requestData.uid; responseData.data.account.token = requestData.token; responseData.data.account.email = account.getEmail(); + if (responseData.data.account.email == null) { // null will trigger crash in some client + responseData.data.account.email = ""; + } Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s", req.ip(), responseData.data.account.uid)); @@ -437,7 +440,7 @@ public final class DispatchServer { // hk4e-sdk-os.hoyoverse.com httpServer.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); // hk4e-sdk-os.hoyoverse.com - httpServer.post("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); + httpServer.get("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); // Game data // hk4e-api-os.hoyoverse.com @@ -469,7 +472,7 @@ public final class DispatchServer { // log-upload-os.mihoyo.com httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + httpServer.post("/sdk/dataUpload", (req, res) -> res.send("Hello")); // /perf/config/verify?device_id=xxx&platform=x&name=xxx httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); @@ -479,6 +482,9 @@ public final class DispatchServer { // log-upload-os.mihoyo.com httpServer.all("/crash/dataUpload", new ClientLogHandler()); + // webstatic-sea.hoyoverse.com + httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); + httpServer.get("/gacha", (req, res) -> res.send("Gacha")); httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); From 1ac5aeb2863070983eafd9e1a9159d12651a66f2 Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sat, 30 Apr 2022 21:25:08 -0700 Subject: [PATCH 016/434] Revert changes for debugging purpose --- .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index c8902e3f6..65ded023e 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -472,7 +472,7 @@ public final class DispatchServer { // log-upload-os.mihoyo.com httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.post("/sdk/dataUpload", (req, res) -> res.send("Hello")); + httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); // /perf/config/verify?device_id=xxx&platform=x&name=xxx httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); From 7b22b575b71e46d5feceeb7f5a12f2e6e1fc7cce Mon Sep 17 00:00:00 2001 From: Kinesis Date: Sun, 1 May 2022 12:43:44 +0800 Subject: [PATCH 017/434] implement McoinExchange packet Handler --- proto/McoinExchangeHcoinReq.proto | 8 ++++++ proto/McoinExchangeHcoinRsp.proto | 8 ++++++ .../recv/HandlerMcoinExchangeHcoinReq.java | 26 +++++++++++++++++++ .../send/PacketMcoinExchangeHcoinRsp.java | 18 +++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 proto/McoinExchangeHcoinReq.proto create mode 100644 proto/McoinExchangeHcoinRsp.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java diff --git a/proto/McoinExchangeHcoinReq.proto b/proto/McoinExchangeHcoinReq.proto new file mode 100644 index 000000000..5b6f37ee9 --- /dev/null +++ b/proto/McoinExchangeHcoinReq.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message McoinExchangeHcoinReq { + uint32 mCoinNum = 1; + uint32 hCoinNum = 2; +} diff --git a/proto/McoinExchangeHcoinRsp.proto b/proto/McoinExchangeHcoinRsp.proto new file mode 100644 index 000000000..b1904885b --- /dev/null +++ b/proto/McoinExchangeHcoinRsp.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message McoinExchangeHcoinRsp { + uint32 mCoinNum = 1; + uint32 hCoinNum = 2; +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java new file mode 100644 index 000000000..0f3d2e7f8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.McoinExchangeHcoinReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketMcoinExchangeHcoinRsp; + +@Opcodes(PacketOpcodes.McoinExchangeHcoinReq) +public class HandlerMcoinExchangeHcoinReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq exchangeReq = McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq.parseFrom(payload); + if (session == null) { + return; + } else if (session.getPlayer().getCrystals() < exchangeReq.getMCoinNum()) { + return; + } + session.getPlayer().setCrystals(session.getPlayer().getCrystals() - exchangeReq.getMCoinNum()); + session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() + exchangeReq.getHCoinNum()); + session.getPlayer().save(); + session.send(new PacketMcoinExchangeHcoinRsp(session.getPlayer().getCrystals(), session.getPlayer().getPrimogems())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java new file mode 100644 index 000000000..65bef7b27 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.McoinExchangeHcoinRspOuterClass; + +public class PacketMcoinExchangeHcoinRsp extends BasePacket { + + public PacketMcoinExchangeHcoinRsp(int mcoin, int hcoin) { + super(PacketOpcodes.McoinExchangeHcoinRsp); + + McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp mcoinExchangeHcoinRsp = McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp.newBuilder() + .setMCoinNum(mcoin) + .setHCoinNum(hcoin).build(); + + this.setData(mcoinExchangeHcoinRsp); + } +} From 66ccd304168bd7264622de5abdbbfd23a502f124 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sat, 30 Apr 2022 22:51:21 -0700 Subject: [PATCH 018/434] Fix possible exploit with mcoin exchange --- .../server/packet/recv/HandlerMcoinExchangeHcoinReq.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java index 0f3d2e7f8..57ecaf42b 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java @@ -13,14 +13,15 @@ public class HandlerMcoinExchangeHcoinReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq exchangeReq = McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq.parseFrom(payload); - if (session == null) { - return; - } else if (session.getPlayer().getCrystals() < exchangeReq.getMCoinNum()) { + + if (session.getPlayer().getCrystals() < exchangeReq.getMCoinNum() && exchangeReq.getMCoinNum() == exchangeReq.getHCoinNum()) { return; } + session.getPlayer().setCrystals(session.getPlayer().getCrystals() - exchangeReq.getMCoinNum()); session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() + exchangeReq.getHCoinNum()); session.getPlayer().save(); + session.send(new PacketMcoinExchangeHcoinRsp(session.getPlayer().getCrystals(), session.getPlayer().getPrimogems())); } } From 65c63ac34a77f53ebb8740e49eed0e3557ede9ef Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sat, 30 Apr 2022 22:52:09 -0700 Subject: [PATCH 019/434] Change RunMode and DebugMode to enums --- src/main/java/emu/grasscutter/Config.java | 6 +++-- .../java/emu/grasscutter/Grasscutter.java | 25 +++++++++---------- .../grasscutter/database/DatabaseManager.java | 5 ++-- .../dispatch/DispatchHttpJsonHandler.java | 5 ++-- .../server/dispatch/DispatchServer.java | 10 +++++--- .../server/game/GameServerPacketHandler.java | 3 ++- .../grasscutter/server/game/GameSession.java | 5 ++-- .../packet/send/PacketPlayerLoginRsp.java | 4 ++- 8 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index bc691dbd9..5a49c325c 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -1,5 +1,7 @@ package emu.grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.game.mail.Mail; public final class Config { @@ -15,8 +17,8 @@ public final class Config { public String SCRIPTS_FOLDER = "./resources/Scripts/"; public String PLUGINS_FOLDER = "./plugins/"; - public String DebugMode = "NONE"; // ALL, MISSING, NONE - public String RunMode = "HYBRID"; // HYBRID, DISPATCH_ONLY, GAME_ONLY + public ServerDebugMode DebugMode = ServerDebugMode.NONE; // ALL, MISSING, NONE + public ServerRunMode RunMode = ServerRunMode.HYBRID; // HYBRID, DISPATCH_ONLY, GAME_ONLY public GameServerOptions GameServer = new GameServerOptions(); public DispatchServerOptions DispatchServer = new DispatchServerOptions(); diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 60d042b11..c7ab7c637 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -35,7 +35,6 @@ public final class Grasscutter { private static int day; // Current day of week - public static RunMode MODE = RunMode.BOTH; private static DispatchServer dispatchServer; private static GameServer gameServer; private static PluginManager pluginManager; @@ -58,8 +57,6 @@ public final class Grasscutter { for (String arg : args) { switch (arg.toLowerCase()) { - case "-auth" -> MODE = RunMode.AUTH; - case "-game" -> MODE = RunMode.GAME; case "-handbook" -> { Tools.createGmHandbook(); return; } @@ -85,12 +82,12 @@ public final class Grasscutter { gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); // Start servers. - if(getConfig().RunMode.equalsIgnoreCase("HYBRID")) { + if (getConfig().RunMode == ServerRunMode.HYBRID) { dispatchServer.start(); gameServer.start(); - } else if (getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { + } else if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { dispatchServer.start(); - } else if (getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + } else if (getConfig().RunMode == ServerRunMode.GAME_ONLY) { gameServer.start(); } else { getLogger().error("Invalid server run mode. " + getConfig().RunMode); @@ -140,7 +137,7 @@ 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")) { + if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { getLogger().error("Commands are not supported in dispatch only mode."); return; } @@ -154,12 +151,6 @@ public final class Grasscutter { Grasscutter.getLogger().error("An error occurred.", e); } } - - public enum RunMode { - BOTH, - AUTH, - GAME - } public static Config getConfig() { return config; @@ -193,4 +184,12 @@ public final class Grasscutter { public static int getCurrentDayOfWeek() { return day; } + + public enum ServerRunMode { + HYBRID, DISPATCH_ONLY, GAME_ONLY + } + + public enum ServerDebugMode { + ALL, MISSING, NONE + } } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 68e1f5e87..c6a5f329a 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -12,6 +12,7 @@ import dev.morphia.Morphia; import dev.morphia.mapping.MapperOptions; import dev.morphia.query.experimental.filters.Filters; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.game.Account; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.friends.Friendship; @@ -41,7 +42,7 @@ public final class DatabaseManager { // 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")) { + if(Grasscutter.getConfig().RunMode == ServerRunMode.GAME_ONLY) { return dispatchDatastore; } else { return datastore; @@ -77,7 +78,7 @@ public final class DatabaseManager { } } - if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + if(Grasscutter.getConfig().RunMode == ServerRunMode.GAME_ONLY) { dispatchMongoClient = MongoClients.create(Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseUrl); dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseCollection); diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index 80e582b5f..31abc77ae 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -8,6 +8,7 @@ import java.util.Collections; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; import express.http.HttpContextHandler; import express.http.Request; import express.http.Response; @@ -34,8 +35,8 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { @Override public void handle(Request req, Response res) throws IOException { // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled - if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING") && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { - Grasscutter.getLogger().info(String.format("[Dispatch] Client %s %s request: ", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING") ? "(MISSING)" : "")); + if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s %s request: ", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); } res.send(response); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 65ded023e..9fc85b477 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -6,6 +6,8 @@ import com.google.protobuf.ByteString; import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; @@ -62,7 +64,7 @@ public final class DispatchServer { public QueryCurrRegionHttpRsp getCurrRegion() { // Needs to be fixed by having the game servers connect to the dispatch server. - if (Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { + if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) { return regions.get(defaultServerName).parsedRegionQuery; } @@ -98,7 +100,7 @@ public final class DispatchServer { 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 + if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) { // Automatically add the game server if in // hybrid mode RegionSimpleInfo server = RegionSimpleInfo.newBuilder() .setName("os_usa") @@ -233,7 +235,7 @@ public final class DispatchServer { }); config.enforceSsl = Grasscutter.getConfig().getDispatchOptions().UseSSL; - if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("ALL")) { + if(Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { config.enableDevLogging(); } }); @@ -241,7 +243,7 @@ public final class DispatchServer { httpServer.get("/", (req, res) -> res.send("Welcome to Grasscutter")); httpServer.raw().error(404, ctx -> { - if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING")) { + if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { Grasscutter.getLogger().info(String.format("[Dispatch] Potential unhandled %s request: %s", ctx.method(), ctx.url())); } ctx.contentType("text/html"); diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 006d4621f..89fa71481 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -6,6 +6,7 @@ import emu.grasscutter.server.event.game.ReceivePacketEvent; import org.reflections.Reflections; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; @@ -88,7 +89,7 @@ public class GameServerPacketHandler { } // Log unhandled packets - if (Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING")) { + if (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + emu.grasscutter.net.packet.PacketOpcodesUtil.getOpcodeName(opcode)); } } diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index a980a377c..1ae5067ea 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.Set; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.game.Account; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; @@ -163,7 +164,7 @@ public class GameSession extends KcpChannel { } // Log - if (Grasscutter.getConfig().DebugMode.equalsIgnoreCase("ALL")) { + if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { logPacket(packet); } @@ -230,7 +231,7 @@ public class GameSession extends KcpChannel { } // Log packet - if (Grasscutter.getConfig().DebugMode.equalsIgnoreCase("ALL")) { + if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { if (!loopPacket.contains(opcode)) { Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")"); System.out.println(Utils.bytesToHex(payload)); 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 18ff6c03e..6407e9412 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -2,6 +2,8 @@ package emu.grasscutter.server.packet.send; import com.google.protobuf.ByteString; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.PlayerLoginRspOuterClass.PlayerLoginRsp; @@ -25,7 +27,7 @@ public class PacketPlayerLoginRsp extends BasePacket { RegionInfo info; - if(Grasscutter.getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { + if (Grasscutter.getConfig().RunMode == ServerRunMode.GAME_ONLY) { if (regionCache == null) { try { File file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt"); From 8a655c16eb0425bb5a6ad096e69df3808bf2e92c Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sat, 30 Apr 2022 22:11:42 -0700 Subject: [PATCH 020/434] Fix crash and revise route * Fix another point which will trigger iOS client to crash * Revise `compareProtocolVersion` route to handle all method --- .../emu/grasscutter/server/dispatch/DispatchServer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 9fc85b477..5ef8f7a89 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -319,6 +319,9 @@ public final class DispatchServer { responseData.data.account.uid = account.getId(); responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); + if (responseData.data.account.email == null) { + responseData.data.account.email = ""; + } Grasscutter.getLogger() .info(String.format("[Dispatch] Client %s failed to log in: Account %s created", @@ -442,7 +445,8 @@ public final class DispatchServer { // hk4e-sdk-os.hoyoverse.com httpServer.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); // hk4e-sdk-os.hoyoverse.com - httpServer.get("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); + // this could be either GET or POST based on the observation of different clients + httpServer.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); // Game data // hk4e-api-os.hoyoverse.com From 15d2795686b6abbdbd0d2e078dac93f620a8f75b Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sat, 30 Apr 2022 22:30:47 -0700 Subject: [PATCH 021/434] Should fix all crashes when login for iOS client --- .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 5ef8f7a89..1a59cb9ad 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -346,6 +346,9 @@ public final class DispatchServer { responseData.data.account.uid = account.getId(); responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); + if (responseData.data.account.email == null) { + responseData.data.account.email = ""; + } Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in as %s", req.ip(), responseData.data.account.uid)); From d050407421e56431fb814787c82410802f62ed44 Mon Sep 17 00:00:00 2001 From: mikuyourworld <121100193@qq.com> Date: Sun, 1 May 2022 15:07:58 +0900 Subject: [PATCH 022/434] Update readme because the commad /clearartifacts and /clearweapons have been merged in this PR https://github.com/Grasscutters/Grasscutter/pull/245 --- README.md | 3 +-- README_zh-CN.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 623baf68b..dc87ff1d8 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,7 @@ There is a dummy user named "Server" in every player's friends list that you can | broadcast | broadcast \ | server.broadcast | Both side | Sends a message to all the players. | b | | coop | coop \ \ | server.coop | Both side | Forces someone to join the world of others. | | | changescene | changescene \ | player.changescene | Client only | Switch scenes by scene ID. | scene | -| clearartifacts | clearartifacts | player.clearartifacts | Client only | Deletes all unequipped and unlocked level 0 artifacts, including 5-star rarity ones from your inventory. | clearart | -| clearweapons | clearweapons | player.clearweapons | Client only | Deletes all unequipped and unlocked weapons, including 5-star rarity ones from your inventory. | clearwpns | +| clear | clear [UID] | player.clearinv | Client only | Deletes all unequipped and unlocked level 0 artifacts(art)/weapons(wp)/material(all) or all, including 5-star rarity ones from your inventory. | clear | | drop | drop [amount] | server.drop | Client only | Drops an item around you. | `d` `dropitem` | | give | give [player] [amount] [level] [finement] | player.give | Both side | Gives item(s) to you or the specified player. (finement option only weapon.) | `g` `item` `giveitem` | | givechar | givechar \ \ | player.givechar | Both side | Gives the player a specified character. | givec | diff --git a/README_zh-CN.md b/README_zh-CN.md index 2558d52fc..9a58b9565 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -111,8 +111,7 @@ chmod +x gradlew | broadcast | broadcast <消息内容> | server.broadcast | 均可使用 | 给所有玩家发送公告 | b | | coop | coop \ <目标uid> | server.coop | 均可使用 | 强制某位玩家进入指定玩家的多人世界 | | | changescene | changescene <场景ID> | player.changescene | 仅客户端 | 切换到指定场景 | scene | -| clearartifacts | clearartifacts | player.clearartifacts | 仅客户端 | 删除所有未装备及未解锁的圣遗物,包括五星 | clearart | -| clearweapons | clearweapons | player.clearweapons | 仅客户端 | 删除所有未装备及未解锁的武器,包括五星 | clearwp | +| clear | clear [UID] | player.clearinv | 仅客户端 | 删除所有未装备及未解锁的圣遗物(art)或武器(wp)或材料(mat)或者所有(all),包括五星 | clear | | drop | drop <物品ID\|物品名称> [数量] | server.drop | 仅客户端 | 在指定玩家周围掉落指定物品 | `d` `dropitem` | | give | give [uid] <物品ID\|物品名称> [数量] [等级] [精炼等级] | | | 给予指定玩家一定数量及等级的物品 (精炼等级仅适用于武器) | `g` `item` `giveitem` | | givechar | givechar \ <角色ID> [等级] | player.givechar | 均可使用 | 给予指定玩家对应角色 | givec | From d484ba7ed6f3592d879e36ea2a370f4ddc4d7c4f Mon Sep 17 00:00:00 2001 From: 4Benj_ Date: Sun, 1 May 2022 14:31:39 +0800 Subject: [PATCH 023/434] Cleaned up dispatch iOS fixes (#396) * Attempting to fix crashing on iOS devices plus I forgot a thing in string.format * Removed unnecessary things --- src/main/java/emu/grasscutter/game/Account.java | 6 +++++- .../server/dispatch/DispatchHttpJsonHandler.java | 2 +- .../emu/grasscutter/server/dispatch/DispatchServer.java | 9 --------- .../server/dispatch/json/LoginResultJson.java | 2 +- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 7ccaf7e7e..5b8523ec3 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -74,7 +74,11 @@ public class Account { } public String getEmail() { - return email; + if(email != null && !email.isEmpty()) { + return email; + } else { + return ""; + } } public void setEmail(String email) { diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index 31abc77ae..58d7207c5 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -36,7 +36,7 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { public void handle(Request req, Response res) throws IOException { // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { - Grasscutter.getLogger().info(String.format("[Dispatch] Client %s %s request: ", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s %s request: %s", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); } res.send(response); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 1a59cb9ad..b7e1e34ee 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -319,9 +319,6 @@ public final class DispatchServer { responseData.data.account.uid = account.getId(); responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); - if (responseData.data.account.email == null) { - responseData.data.account.email = ""; - } Grasscutter.getLogger() .info(String.format("[Dispatch] Client %s failed to log in: Account %s created", @@ -346,9 +343,6 @@ public final class DispatchServer { responseData.data.account.uid = account.getId(); responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); - if (responseData.data.account.email == null) { - responseData.data.account.email = ""; - } Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in as %s", req.ip(), responseData.data.account.uid)); @@ -389,9 +383,6 @@ public final class DispatchServer { responseData.data.account.uid = requestData.uid; responseData.data.account.token = requestData.token; responseData.data.account.email = account.getEmail(); - if (responseData.data.account.email == null) { // null will trigger crash in some client - responseData.data.account.email = ""; - } Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s", req.ip(), responseData.data.account.uid)); 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 88e142d4f..1f4dcd4b4 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java +++ b/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java @@ -16,7 +16,7 @@ public class LoginResultJson { public static class VerifyAccountData { public String uid; public String name = ""; - public String email; + public String email = ""; public String mobile = ""; public String is_email_verify = "0"; public String realname = ""; From ec09bc28f2c55a3babfcedec69a233504af64a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E5=82=91?= Date: Sun, 1 May 2022 22:42:02 +0800 Subject: [PATCH 024/434] Fixed can set talent level to 16 bug (#408) Level should be **lower than 16** --- .../java/emu/grasscutter/command/commands/TalentCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index f0e779700..32e2f9ee8 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -53,7 +53,7 @@ public final class TalentCommand implements CommandHandler { CommandHandler.sendMessage(sender, "To get talent ID: /talent getid"); return; } - if (nextLevel > 16){ + if (nextLevel >= 16){ CommandHandler.sendMessage(sender, "Invalid talent level. Level should be lower than 16"); return; } @@ -117,7 +117,7 @@ public final class TalentCommand implements CommandHandler { CommandHandler.sendMessage(sender, "To set talent level: /talent "); return; } - if (nextLevel > 16){ + if (nextLevel >= 16){ CommandHandler.sendMessage(sender, "Invalid talent level. Level should be lower than 16"); return; } From d4e1b265e31f16780897152250ab9df6076983cc Mon Sep 17 00:00:00 2001 From: Kinesis Date: Sun, 1 May 2022 16:24:23 +0800 Subject: [PATCH 025/434] fix McoinExchangeHcoinRsp packet structure --- proto/McoinExchangeHcoinRsp.proto | 3 +-- .../packet/recv/HandlerMcoinExchangeHcoinReq.java | 5 +++-- .../packet/send/PacketMcoinExchangeHcoinRsp.java | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/proto/McoinExchangeHcoinRsp.proto b/proto/McoinExchangeHcoinRsp.proto index b1904885b..090341308 100644 --- a/proto/McoinExchangeHcoinRsp.proto +++ b/proto/McoinExchangeHcoinRsp.proto @@ -3,6 +3,5 @@ syntax = "proto3"; option java_package = "emu.grasscutter.net.proto"; message McoinExchangeHcoinRsp { - uint32 mCoinNum = 1; - uint32 hCoinNum = 2; + int32 retcode = 1; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java index 57ecaf42b..37dc0fcc7 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMcoinExchangeHcoinReq.java @@ -4,6 +4,7 @@ import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.McoinExchangeHcoinReqOuterClass; +import emu.grasscutter.net.proto.RetcodeOuterClass; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketMcoinExchangeHcoinRsp; @@ -15,13 +16,13 @@ public class HandlerMcoinExchangeHcoinReq extends PacketHandler { McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq exchangeReq = McoinExchangeHcoinReqOuterClass.McoinExchangeHcoinReq.parseFrom(payload); if (session.getPlayer().getCrystals() < exchangeReq.getMCoinNum() && exchangeReq.getMCoinNum() == exchangeReq.getHCoinNum()) { + session.send(new PacketMcoinExchangeHcoinRsp(RetcodeOuterClass.Retcode.RET_UNKNOWN_ERROR_VALUE)); return; } session.getPlayer().setCrystals(session.getPlayer().getCrystals() - exchangeReq.getMCoinNum()); session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() + exchangeReq.getHCoinNum()); session.getPlayer().save(); - - session.send(new PacketMcoinExchangeHcoinRsp(session.getPlayer().getCrystals(), session.getPlayer().getPrimogems())); + session.send(new PacketMcoinExchangeHcoinRsp(RetcodeOuterClass.Retcode.RET_SUCC_VALUE)); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java index 65bef7b27..a37c12505 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMcoinExchangeHcoinRsp.java @@ -6,13 +6,13 @@ import emu.grasscutter.net.proto.McoinExchangeHcoinRspOuterClass; public class PacketMcoinExchangeHcoinRsp extends BasePacket { - public PacketMcoinExchangeHcoinRsp(int mcoin, int hcoin) { + public PacketMcoinExchangeHcoinRsp(int retCode) { super(PacketOpcodes.McoinExchangeHcoinRsp); - McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp mcoinExchangeHcoinRsp = McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp.newBuilder() - .setMCoinNum(mcoin) - .setHCoinNum(hcoin).build(); + McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp proto = McoinExchangeHcoinRspOuterClass.McoinExchangeHcoinRsp.newBuilder() + .setRetcode(retCode) + .build(); - this.setData(mcoinExchangeHcoinRsp); + this.setData(proto); } } From 59d5f4feec2242eb68d768b1cf9f27aa3fd3b1e9 Mon Sep 17 00:00:00 2001 From: coooookies <1164557342@qq.com> Date: Sun, 1 May 2022 18:41:51 +0800 Subject: [PATCH 026/434] GameServerPacketHandler need to be added a registration interface for plugin developers --- .../grasscutter/server/game/GameServerPacketHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 89fa71481..dbead5240 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -22,7 +22,11 @@ public class GameServerPacketHandler { this.registerHandlers(handlerClass); } - + + public void registerPacketHandler(int opcode, PacketHandler handler) { + this.handlers.put(opcode, handler); + } + public void registerHandlers(Class handlerClass) { Reflections reflections = new Reflections("emu.grasscutter.server.packet"); Set handlerClasses = reflections.getSubTypesOf(handlerClass); From 1e166960d2cfdf6f94a64b5b4ae14626ddd99c15 Mon Sep 17 00:00:00 2001 From: coooookies <1164557342@qq.com> Date: Sun, 1 May 2022 20:47:54 +0800 Subject: [PATCH 027/434] Improve registration methods. --- .../server/game/GameServerPacketHandler.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index dbead5240..35303b639 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -23,8 +23,20 @@ public class GameServerPacketHandler { this.registerHandlers(handlerClass); } - public void registerPacketHandler(int opcode, PacketHandler handler) { - this.handlers.put(opcode, handler); + public void registerPacketHandler(Class handlerClass) { + try { + Opcodes opcode = handlerClass.getAnnotation(Opcodes.class); + + if (opcode == null || opcode.disabled() || opcode.value() <= 0) { + return; + } + + PacketHandler packetHandler = (PacketHandler) handlerClass.newInstance(); + + this.handlers.put(opcode.value(), packetHandler); + } catch (Exception e) { + e.printStackTrace(); + } } public void registerHandlers(Class handlerClass) { @@ -32,21 +44,7 @@ public class GameServerPacketHandler { Set handlerClasses = reflections.getSubTypesOf(handlerClass); for (Object obj : handlerClasses) { - Class c = (Class) obj; - - try { - Opcodes opcode = c.getAnnotation(Opcodes.class); - - if (opcode == null || opcode.disabled() || opcode.value() <= 0) { - continue; - } - - PacketHandler packetHandler = (PacketHandler) c.newInstance(); - - this.handlers.put(opcode.value(), packetHandler); - } catch (Exception e) { - e.printStackTrace(); - } + this.registerPacketHandler((Class) obj); } // Debug From 2c0576f697a249ba028085af946a33576a0df7ec Mon Sep 17 00:00:00 2001 From: JimWails <1142247734@qq.com> Date: Sun, 1 May 2022 23:02:18 +0800 Subject: [PATCH 028/434] Fixed can set avatar level more than 90 and cause game to freeze Limit the avatar level given by "/givechar" command. If avatar level >90, the game will freeze if open the character interface. --- .../emu/grasscutter/command/commands/GiveCharCommand.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index a15a9b95c..70f051ef4 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -73,6 +73,12 @@ public final class GiveCharCommand implements CommandHandler { return; } + // Check level. + if (level > 90) { + CommandHandler.sendMessage(sender, "Invalid avatar level."); + return; + } + // Calculate ascension level. if (level <= 40) { ascension = (int) Math.ceil(level / 20f); @@ -88,6 +94,6 @@ public final class GiveCharCommand implements CommandHandler { avatar.recalcStats(); targetPlayer.addAvatar(avatar); - CommandHandler.sendMessage(sender, String.format("Given %s to %s.", avatarId, target)); + CommandHandler.sendMessage(sender, String.format("Given %s with level %s to %s.", avatarId, level, target)); } } From 29c95cb1b67de6f8a5353f3087284b72956dcb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E5=82=91?= Date: Mon, 2 May 2022 02:22:29 +0800 Subject: [PATCH 029/434] Add `/setstats mhp` to set Max HP (#407) * Fixed `/setstats hp` without changing the max hp. The Max HP should be modified. * Add `/setstats mhp` to set Max HP --- .../command/commands/SetStatsCommand.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index 6e18573bb..0ca3f07ce 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -28,9 +28,21 @@ public final class SetStatsCommand implements CommandHandler { String stat = args.get(0); switch (stat) { default: - CommandHandler.sendMessage(sender, "Usage: /setstats|stats for basic stats"); + CommandHandler.sendMessage(sender, "Usage: /setstats|stats for basic stats"); CommandHandler.sendMessage(sender, "Usage: /stats for elemental bonus"); return; + case "mhp": + try { + int health = Integer.parseInt(args.get(1)); + EntityAvatar entity = sender.getTeamManager().getCurrentAvatarEntity(); + entity.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, health); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_MAX_HP)); + CommandHandler.sendMessage(sender, "MAX HP set to " + health + "."); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, "Invalid Max HP value."); + return; + } + break; case "hp": try { int health = Integer.parseInt(args.get(1)); From 6d969064843dd934f6ece36bab6681a2376ff002 Mon Sep 17 00:00:00 2001 From: xtaodada Date: Mon, 2 May 2022 04:15:32 +0800 Subject: [PATCH 030/434] Show shopmall --- .../server/packet/send/PacketGetShopmallDataRsp.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopmallDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopmallDataRsp.java index 17f682406..d380c2953 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopmallDataRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopmallDataRsp.java @@ -4,13 +4,19 @@ import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.GetShopmallDataRspOuterClass.GetShopmallDataRsp; +import java.util.List; + public class PacketGetShopmallDataRsp extends BasePacket { public PacketGetShopmallDataRsp() { super(PacketOpcodes.GetShopmallDataRsp); - GetShopmallDataRsp proto = GetShopmallDataRsp.newBuilder().build(); - + List shop_malls = List.of(900, 1052, 902, 1001, 903); + + GetShopmallDataRsp proto = GetShopmallDataRsp.newBuilder() + .addAllShopTypeList(shop_malls) + .build(); + this.setData(proto); } } From 22a651b4aaed18755357e692a26bf9901a6ba519 Mon Sep 17 00:00:00 2001 From: xtaodada Date: Mon, 2 May 2022 04:28:38 +0800 Subject: [PATCH 031/434] Fix goods limit bug --- .../emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java index e0257c4c8..0088b7176 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java @@ -54,7 +54,7 @@ public class HandlerBuyGoodsReq extends PacketHandler { session.getPlayer().save(); } - if (bought + buyGoodsReq.getBoughtNum() > sg.getBuyLimit()) { + if ((bought + buyGoodsReq.getBoughtNum() > sg.getBuyLimit()) && sg.getBuyLimit() != 0) { return; } From 8cf4ef59ec8f1986ed49e582ef30cf95ddccd37c Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sun, 1 May 2022 12:49:44 -0700 Subject: [PATCH 032/434] Implement gacha history record subsystem * Frontend is not very beautiful yet * Didn't include too much `some anime game` data in the page to avoid being DMCA'd --- data/gacha_records.html | 176 ++++++++++++++++++ .../grasscutter/database/DatabaseHelper.java | 39 ++++ .../grasscutter/database/DatabaseManager.java | 3 +- .../grasscutter/game/gacha/GachaBanner.java | 16 +- .../grasscutter/game/gacha/GachaManager.java | 22 ++- .../grasscutter/game/gacha/GachaRecord.java | 75 ++++++++ .../server/dispatch/DispatchServer.java | 3 +- .../server/http/gacha/GachaRecordHandler.java | 52 ++++++ .../packet/recv/HandlerGetGachaInfoReq.java | 5 +- .../packet/send/PacketGetGachaInfoRsp.java | 8 + 10 files changed, 393 insertions(+), 6 deletions(-) create mode 100644 data/gacha_records.html create mode 100644 src/main/java/emu/grasscutter/game/gacha/GachaRecord.java create mode 100644 src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java diff --git a/data/gacha_records.html b/data/gacha_records.html new file mode 100644 index 000000000..21270a046 --- /dev/null +++ b/data/gacha_records.html @@ -0,0 +1,176 @@ + + + + + + + +

+ + + + + \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index b2dae0446..2a247b46e 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -3,11 +3,14 @@ package emu.grasscutter.database; import java.util.List; import com.mongodb.client.result.DeleteResult; +import dev.morphia.query.FindOptions; +import dev.morphia.query.Sort; import dev.morphia.query.experimental.filters.Filters; import emu.grasscutter.GameConstants; import emu.grasscutter.game.Account; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.friends.Friendship; +import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.player.Player; @@ -78,6 +81,11 @@ public final class DatabaseHelper { return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("token", token)).first(); } + public static Account getAccountBySessionKey(String sessionKey) { + if(sessionKey == null) return null; + return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first(); + } + public static Account getAccountById(String uid) { return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first(); } @@ -181,5 +189,36 @@ public final class DatabaseHelper { )).first(); } + public static List getGachaRecords(int ownerId, int page, int gachaType){ + return getGachaRecords(ownerId, page, gachaType, 10); + } + + public static List getGachaRecords(int ownerId, int page, int gachaType, int pageSize){ + return DatabaseManager.getDatastore().find(GachaRecord.class).filter( + Filters.eq("ownerId", ownerId), + Filters.eq("gachaType", gachaType) + ).iterator(new FindOptions() + .sort(Sort.descending("transactionDate")) + .skip(pageSize * page) + .limit(pageSize) + ).toList(); + } + + public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType){ + return getGachaRecordsMaxPage(ownerId, page, gachaType, 10); + } + + public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize){ + long count = DatabaseManager.getDatastore().find(GachaRecord.class).filter( + Filters.eq("ownerId", ownerId), + Filters.eq("gachaType", gachaType) + ).count(); + return count / 10 + (count % 10 > 0 ? 1 : 0 ); + } + + public static void saveGachaRecord(GachaRecord gachaRecord){ + DatabaseManager.getDatastore().save(gachaRecord); + } + public static char AWJVN = 'e'; } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index c6a5f329a..2376451db 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -16,6 +16,7 @@ import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.game.Account; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.friends.Friendship; +import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.player.Player; @@ -28,7 +29,7 @@ public final class DatabaseManager { private static Datastore dispatchDatastore; private static final Class[] mappedClasses = new Class[] { - DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class + DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class }; public static Datastore getDatastore() { diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 9b54c924f..2317af38e 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -91,9 +91,21 @@ public class GachaBanner { return eventChance; } + @Deprecated public GachaInfo toProto() { - String record = "http://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + "/gacha"; - + return toProto(""); + } + public GachaInfo toProto(String sessionKey) { + String record = "https://" + + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? + Grasscutter.getConfig().getDispatchOptions().Ip : + Grasscutter.getConfig().getDispatchOptions().PublicIp) + + ":" + + Integer.toString(Grasscutter.getConfig().getDispatchOptions().PublicPort == 0 ? + Grasscutter.getConfig().getDispatchOptions().Port : + Grasscutter.getConfig().getDispatchOptions().PublicPort) + + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; + // Grasscutter.getLogger().info("record = " + record); GachaInfo.Builder info = GachaInfo.newBuilder() .setGachaType(this.getGachaType()) .setScheduleId(this.getScheduleId()) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 5cd484e9a..cd1b5ea94 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -14,6 +14,7 @@ import com.sun.nio.file.SensitivityWatchEventModifier; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.gacha.GachaBanner.BannerType; import emu.grasscutter.game.inventory.GameItem; @@ -196,6 +197,10 @@ public class GachaManager { if (itemData == null) { continue; } + + // Write gacha record + GachaRecord gachaRecord = new GachaRecord(itemId, player.getUid(), gachaType); + DatabaseHelper.saveGachaRecord(gachaRecord); // Create gacha item GachaItem.Builder gachaItem = GachaItem.newBuilder(); @@ -321,6 +326,7 @@ public class GachaManager { } } + @Deprecated private synchronized GetGachaInfoRsp createProto() { GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345); @@ -330,12 +336,26 @@ public class GachaManager { return proto.build(); } + + private synchronized GetGachaInfoRsp createProto(String sessionKey) { + GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345); + + for (GachaBanner banner : getGachaBanners().values()) { + proto.addGachaInfoList(banner.toProto(sessionKey)); + } + + return proto.build(); + } + @Deprecated public GetGachaInfoRsp toProto() { if (this.cachedProto == null) { this.cachedProto = createProto(); } - return this.cachedProto; } + + public GetGachaInfoRsp toProto(String sessionKey) { + return createProto(sessionKey); + } } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaRecord.java b/src/main/java/emu/grasscutter/game/gacha/GachaRecord.java new file mode 100644 index 000000000..ffaf983b6 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/gacha/GachaRecord.java @@ -0,0 +1,75 @@ +package emu.grasscutter.game.gacha; + +import java.util.Date; + +import org.bson.types.ObjectId; + +import dev.morphia.annotations.*; + +@Entity(value = "gachas", useDiscriminator = false) +public class GachaRecord { + @Id private ObjectId id; + + @Indexed private int ownerId; + + private Date transactionDate; + private int itemID; + @Indexed private int gachaType; + + public GachaRecord() {} + + public GachaRecord(int itemId ,int ownerId, int gachaType){ + this.transactionDate = new Date(); + this.itemID = itemId; + this.ownerId = ownerId; + this.gachaType = gachaType; + } + + public int getOwnerId() { + return ownerId; + } + + public void setOwnerId(int ownerId) { + this.ownerId = ownerId; + } + + public int getGachaType() { + return gachaType; + } + + public void setGachaType(int type) { + this.gachaType = type; + } + + public Date getTransactionDate() { + return transactionDate; + } + + public void setTransactionDate(Date transactionDate) { + this.transactionDate = transactionDate; + } + + public int getItemID() { + return itemID; + } + + public void setItemID(int itemID) { + this.itemID = itemID; + } + + public ObjectId getId(){ + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String toString() { + return toJsonString(); + } + public String toJsonString() { + return "{\"time\": " + this.transactionDate.getTime() + ",\"item\":" + this.itemID + "}"; + } + +} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index b7e1e34ee..7b5418e15 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -18,6 +18,7 @@ import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; +import emu.grasscutter.server.http.gacha.GachaRecordHandler; import emu.grasscutter.utils.FileUtils; import express.Express; import org.eclipse.jetty.server.Connector; @@ -485,7 +486,7 @@ public final class DispatchServer { // webstatic-sea.hoyoverse.com httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); - httpServer.get("/gacha", (req, res) -> res.send("Gacha")); + httpServer.get("/gacha", new GachaRecordHandler()); httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + httpServer.raw().port()); diff --git a/src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java new file mode 100644 index 000000000..0798a150f --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java @@ -0,0 +1,52 @@ +package emu.grasscutter.server.http.gacha; + +import java.io.File; +import java.io.IOException; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.utils.FileUtils; +import express.http.HttpContextHandler; +import express.http.Request; +import express.http.Response; + +public final class GachaRecordHandler implements HttpContextHandler { + String render_template; + public GachaRecordHandler() { + File template = new File(Grasscutter.getConfig().DATA_FOLDER + "gacha_records.html"); + if (template.exists()) { + // Load from cache + render_template = new String(FileUtils.read(template)); + } else { + render_template = "{{REPLACE_RECORD}}"; + } + } + + @Override + public void handle(Request req, Response res) throws IOException { + // Grasscutter.getLogger().info( req.query().toString() ); + String sessionKey = req.query("s"); + int page = 0; + int gachaType = 0; + if (req.query("p") != null) { + page = Integer.valueOf(req.query("p")); + } + + if (req.query("gachaType") != null) { + gachaType = Integer.valueOf(req.query("gachaType")); + } + + Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); + if (account != null) { + String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), page, gachaType).toString(); + // Grasscutter.getLogger().info(records); + String response = render_template.replace("{{REPLACE_RECORD}}", records) + .replace("{{REPLACE_MAXPAGE}}", String.valueOf(DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType))); + + res.send(response); + } else { + res.send("404"); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java index 6c4c703a8..76d267b99 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java @@ -11,7 +11,10 @@ public class HandlerGetGachaInfoReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - session.send(new PacketGetGachaInfoRsp(session.getServer().getGachaManager())); + session.send(new PacketGetGachaInfoRsp(session.getServer().getGachaManager(), + // TODO: use other Nonce/key insteadof session key to ensure the overall security for the player + session.getPlayer().getAccount().getSessionKey()) + ); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetGachaInfoRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetGachaInfoRsp.java index 89af334a5..84d857681 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetGachaInfoRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetGachaInfoRsp.java @@ -6,9 +6,17 @@ import emu.grasscutter.net.packet.PacketOpcodes; public class PacketGetGachaInfoRsp extends BasePacket { + @Deprecated public PacketGetGachaInfoRsp(GachaManager manager) { super(PacketOpcodes.GetGachaInfoRsp); this.setData(manager.toProto()); } + + public PacketGetGachaInfoRsp(GachaManager manager, String sessionKey) { + super(PacketOpcodes.GetGachaInfoRsp); + + this.setData(manager.toProto(sessionKey)); + } + } From 3215b6961a1be51b5ec24f17f7fca129d89cf0ed Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sun, 1 May 2022 20:14:47 -0400 Subject: [PATCH 033/434] Change to `xyz.grasscutters` from `tech.xigam` as publish group id --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 9488639e9..e1b8cf1ec 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ plugins { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 -group = 'tech.xigam' +group = 'xyz.grasscutters' version = '1.0.3-dev' sourceCompatibility = 17 @@ -136,9 +136,9 @@ publishing { } developers { developer { - id = 'melledy' - name = 'Melledy' - email = 'melledy@xigam.tech' // not a real email kek + id = 'meledy' + name = 'Meledy' + email = 'meledy@xigam.tech' // not a real email kek } developer { id = 'magix' From 63c7f8d62d20283809e6189ff9f11223787a6985 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Mon, 2 May 2022 02:01:01 -0700 Subject: [PATCH 034/434] Move player mail to MailHandler class This is so we dont have to save the entire player to the db every time we send mail --- .../grasscutter/database/DatabaseHelper.java | 15 +++ .../grasscutter/database/DatabaseManager.java | 3 +- .../java/emu/grasscutter/game/mail/Mail.java | 34 +++++- .../grasscutter/game/mail/MailHandler.java | 104 ++++++++++++++++++ .../emu/grasscutter/game/player/Player.java | 50 +++------ .../server/packet/recv/HandlerDelMailReq.java | 5 +- .../server/packet/send/PacketDelMailRsp.java | 17 +-- 7 files changed, 176 insertions(+), 52 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/mail/MailHandler.java diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 2a247b46e..97bd6d739 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -12,6 +12,7 @@ import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.friends.Friendship; import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; public final class DatabaseHelper { @@ -166,6 +167,7 @@ public final class DatabaseHelper { public static List getInventoryItems(Player player) { return DatabaseManager.getDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); } + public static List getFriends(Player player) { return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); } @@ -219,6 +221,19 @@ public final class DatabaseHelper { public static void saveGachaRecord(GachaRecord gachaRecord){ DatabaseManager.getDatastore().save(gachaRecord); } + + public static List getAllMail(Player player) { + return DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); + } + + public static void saveMail(Mail mail) { + DatabaseManager.getDatastore().save(mail); + } + + public static boolean deleteMail(Mail mail) { + DeleteResult result = DatabaseManager.getDatastore().delete(mail); + return result.wasAcknowledged(); + } public static char AWJVN = 'e'; } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 2376451db..90ff17238 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -18,6 +18,7 @@ import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.friends.Friendship; import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; public final class DatabaseManager { @@ -29,7 +30,7 @@ public final class DatabaseManager { private static Datastore dispatchDatastore; private static final Class[] mappedClasses = new Class[] { - DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class + DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class, Mail.class }; public static Datastore getDatastore() { diff --git a/src/main/java/emu/grasscutter/game/mail/Mail.java b/src/main/java/emu/grasscutter/game/mail/Mail.java index 19fd79adc..286db6ef6 100644 --- a/src/main/java/emu/grasscutter/game/mail/Mail.java +++ b/src/main/java/emu/grasscutter/game/mail/Mail.java @@ -1,15 +1,22 @@ package emu.grasscutter.game.mail; import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import dev.morphia.annotations.Indexed; +import dev.morphia.annotations.Transient; +import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.Player; import java.time.Instant; import java.util.ArrayList; import java.util.List; -@Entity -public class Mail { +import org.bson.types.ObjectId; +@Entity(value = "mail", useDiscriminator = false) +public class Mail { + @Id private ObjectId id; + @Indexed private int ownerUid; public MailContent mailContent; public List itemList; public long sendTime; @@ -18,6 +25,7 @@ public class Mail { public boolean isRead; public boolean isAttachmentGot; public int stateValue; + @Transient private boolean shouldDelete; public Mail() { this(new MailContent(), new ArrayList(), (int) Instant.now().getEpochSecond() + 604800); // TODO: add expire time to send mail command @@ -42,7 +50,19 @@ public class Mail { this.stateValue = state; // Different mailboxes, 1 = Default, 3 = Gift-box. } - @Entity + public ObjectId getId() { + return id; + } + + public int getOwnerUid() { + return ownerUid; + } + + public void setOwnerUid(int ownerUid) { + this.ownerUid = ownerUid; + } + + @Entity public static class MailContent { public String title; public String content; @@ -93,4 +113,12 @@ public class Mail { this.itemLevel = itemLevel; } } + + public void save() { + if (this.expireTime * 1000 < System.currentTimeMillis()) { + DatabaseHelper.deleteMail(this); + } else { + DatabaseHelper.saveMail(this); + } + } } diff --git a/src/main/java/emu/grasscutter/game/mail/MailHandler.java b/src/main/java/emu/grasscutter/game/mail/MailHandler.java new file mode 100644 index 000000000..9d7839fb5 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/mail/MailHandler.java @@ -0,0 +1,104 @@ +package emu.grasscutter.game.mail; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.event.player.PlayerReceiveMailEvent; +import emu.grasscutter.server.packet.send.PacketDelMailRsp; +import emu.grasscutter.server.packet.send.PacketMailChangeNotify; + +public class MailHandler { + private final Player player; + private final List mail; + + public MailHandler(Player player) { + this.player = player; + this.mail = new ArrayList<>(); + } + + public Player getPlayer() { + return player; + } + + public List getMail() { + return mail; + } + + // ---------------------MAIL------------------------ + + public void sendMail(Mail message) { + // Call mail receive event. + PlayerReceiveMailEvent event = new PlayerReceiveMailEvent(this.getPlayer(), message); event.call(); + if(event.isCanceled()) return; message = event.getMessage(); + + message.setOwnerUid(this.getPlayer().getUid()); + + this.mail.add(message); + + Grasscutter.getLogger().debug("Mail sent to user [" + this.getPlayer().getUid() + ":" + this.getPlayer().getNickname() + "]!"); + + if (this.getPlayer().isOnline()) { + this.getPlayer().sendPacket(new PacketMailChangeNotify(this.getPlayer(), message)); + } // TODO: setup a way for the mail notification to show up when someone receives mail when they were offline + } + + public boolean deleteMail(int mailId) { + Mail message = getMailById(mailId); + + if (message != null) { + this.getMail().remove(mailId); + message.expireTime = 0; + message.save(); + + return true; + } + + return false; + } + + public void deleteMail(List mailList) { + List sortedMailList = new ArrayList<>(); + sortedMailList.addAll(mailList); + Collections.sort(sortedMailList, Collections.reverseOrder()); + + List deleted = new ArrayList<>(); + + for (int id : sortedMailList) { + if (this.deleteMail(id)) { + deleted.add(id); + } + } + + player.getSession().send(new PacketDelMailRsp(player, deleted)); + player.getSession().send(new PacketMailChangeNotify(player, null, deleted)); + } + + public Mail getMailById(int index) { return this.mail.get(index); } + + public int getMailIndex(Mail message) { + return this.mail.indexOf(message); + } + + public boolean replaceMailByIndex(int index, Mail message) { + if(getMailById(index) != null) { + this.mail.set(index, message); + message.save(); + return true; + } else { + return false; + } + } + + public void loadFromDatabase() { + List mailList = DatabaseHelper.getAllMail(this.getPlayer()); + + for (Mail mail : mailList) { + this.getMail().add(mail); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 13f2674f2..671e33269 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -19,6 +19,7 @@ import emu.grasscutter.game.gacha.PlayerGachaInfo; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; +import emu.grasscutter.game.mail.MailHandler; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.shop.ShopLimit; @@ -78,14 +79,14 @@ public class Player { @Transient private AvatarStorage avatars; @Transient private Inventory inventory; @Transient private FriendsList friendsList; - + @Transient private MailHandler mailHandler; + private TeamManager teamManager; private PlayerGachaInfo gachaInfo; private PlayerProfile playerProfile; private boolean showAvatar; private ArrayList shownAvatars; private Set rewardedLevels; - private ArrayList mail; private ArrayList shopLimit; private int sceneId; @@ -118,6 +119,7 @@ public class Player { this.inventory = new Inventory(this); this.avatars = new AvatarStorage(this); this.friendsList = new FriendsList(this); + this.mailHandler = new MailHandler(this); this.pos = new Position(); this.rotation = new Position(); this.properties = new HashMap<>(); @@ -133,8 +135,6 @@ public class Player { this.flyCloakList = new HashSet<>(); this.costumeList = new HashSet<>(); - this.mail = new ArrayList<>(); - this.setSceneId(3); this.setRegionId(1); this.sceneState = SceneLoadState.NONE; @@ -437,6 +437,10 @@ public class Player { return this.friendsList; } + public MailHandler getMailHandler() { + return mailHandler; + } + public int getEnterSceneToken() { return enterSceneToken; } @@ -725,47 +729,24 @@ public class Player { // ---------------------MAIL------------------------ - public List getAllMail() { return this.mail; } + public List getAllMail() { return this.getMailHandler().getMail(); } public void sendMail(Mail message) { - // Call mail receive event. - PlayerReceiveMailEvent event = new PlayerReceiveMailEvent(this, message); event.call(); - if(event.isCanceled()) return; message = event.getMessage(); - - this.mail.add(message); - this.save(); - Grasscutter.getLogger().debug("Mail sent to user [" + this.getUid() + ":" + this.getNickname() + "]!"); - if(this.isOnline()) { - this.sendPacket(new PacketMailChangeNotify(this, message)); - } // TODO: setup a way for the mail notification to show up when someone receives mail when they were offline + this.getMailHandler().sendMail(message); } public boolean deleteMail(int mailId) { - Mail message = getMail(mailId); - - if(message != null) { - int index = getMailId(message); - message.expireTime = (int) Instant.now().getEpochSecond(); // Just set the mail as expired for now. I don't want to implement a counter specifically for an account... - this.replaceMailByIndex(index, message); - return true; - } - - return false; + return this.getMailHandler().deleteMail(mailId); } - public Mail getMail(int index) { return this.mail.get(index); } + public Mail getMail(int index) { return this.getMailHandler().getMailById(index); } + public int getMailId(Mail message) { - return this.mail.indexOf(message); + return this.getMailHandler().getMailIndex(message); } public boolean replaceMailByIndex(int index, Mail message) { - if(getMail(index) != null) { - this.mail.set(index, message); - this.save(); - return true; - } else { - return false; - } + return this.getMailHandler().replaceMailByIndex(index, message); } public void interactWith(int gadgetEntityId) { @@ -1015,6 +996,7 @@ public class Player { this.getAvatars().postLoad(); this.getFriendsList().loadFromDatabase(); + this.getMailHandler().loadFromDatabase(); // Create world World world = new World(this); diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java index 4c6473996..2c95cc307 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java @@ -1,5 +1,6 @@ package emu.grasscutter.server.packet.recv; +import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; @@ -14,8 +15,8 @@ public class HandlerDelMailReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { DelMailReqOuterClass.DelMailReq req = DelMailReqOuterClass.DelMailReq.parseFrom(payload); - - session.send(new PacketDelMailRsp(session.getPlayer(), req.getMailIdListList())); + + session.getPlayer().getMailHandler().deleteMail(req.getMailIdListList()); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java index e8348a144..7b618e8d0 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java @@ -13,17 +13,10 @@ public class PacketDelMailRsp extends BasePacket { public PacketDelMailRsp(Player player, List toDeleteIds) { super(PacketOpcodes.DelMailRsp); - DelMailRsp.Builder proto = DelMailRsp.newBuilder(); - - List deletedIds = new ArrayList<>(); - - for(int mailId : toDeleteIds) { - if(player.deleteMail(mailId)) { - deletedIds.add(mailId); - } - } - - this.setData(proto.build()); - player.getSession().send(new PacketMailChangeNotify(player, null, deletedIds)); + DelMailRsp proto = DelMailRsp.newBuilder() + .addAllMailIdList(toDeleteIds) + .build(); + + this.setData(proto); } } \ No newline at end of file From 89bd8a10ef950fae7206cf2e6a0c734a38d54232 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Mon, 2 May 2022 02:01:24 -0700 Subject: [PATCH 035/434] Fix gacha rate for weapons --- src/main/java/emu/grasscutter/game/gacha/GachaManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index cd1b5ea94..ca7640e17 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -134,7 +134,7 @@ public class GachaManager { if (banner.getRateUpItems1().length > 0) { int eventChance = this.randomRange(1, 100); - if (eventChance >= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) { + if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) { itemId = getRandom(banner.getRateUpItems1()); gachaInfo.setFailedFeaturedItemPulls(0); } else { From 50740b3560f7ab8e95cc5cd0d12b0f0eefbc6dbb Mon Sep 17 00:00:00 2001 From: DancingSnow <1121149616@qq.com> Date: Mon, 2 May 2022 16:36:48 +0800 Subject: [PATCH 036/434] fix World level not in 0-8 --- .../grasscutter/command/commands/SetWorldLevelCommand.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index 0ffd015f8..2feb6f64a 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -3,7 +3,6 @@ package emu.grasscutter.command.commands; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.PlayerProperty; import java.util.List; @@ -26,6 +25,10 @@ public final class SetWorldLevelCommand implements CommandHandler { try { int level = Integer.parseInt(args.get(0)); + if (level > 8 || level < 0) { + sender.dropMessage("World level must be between 0-8"); + return; + } // Set in both world and player props sender.getWorld().setWorldLevel(level); From 29b5157d426135fe0873c57bac1845482887fbf0 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Mon, 2 May 2022 17:25:26 +0800 Subject: [PATCH 037/434] Custom Authentication Handler --- .../server/dispatch/DispatchServer.java | 96 ++++++++----------- .../authentication/AuthenticationHandler.java | 16 ++++ .../DefaultAuthenticationHandler.java | 84 ++++++++++++++++ 3 files changed, 139 insertions(+), 57 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java create mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 7b5418e15..5fdda7e87 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -14,6 +14,8 @@ 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.authentication.AuthenticationHandler; +import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; @@ -39,6 +41,7 @@ public final class DispatchServer { public String regionListBase64; public Map regions; + private AuthenticationHandler authHandler; private Express httpServer; public DispatchServer() { @@ -251,6 +254,16 @@ public final class DispatchServer { ctx.result(""); // I'm like 70% sure this won't break anything. }); + // Authentication Handler + // These routes are so that authentication routes are always the same no matter what auth system is used. + httpServer.get("/authentication/type", (req, res) -> { + res.send(this.getAuthHandler().getClass().getName()); + }); + + httpServer.post("/authentication/login", (req, res) -> this.getAuthHandler().handleLogin(req, res)); + httpServer.post("/authentication/register", (req, res) -> this.getAuthHandler().handleRegister(req, res)); + httpServer.post("/authentication/change_password", (req, res) -> this.getAuthHandler().handleChangePassword(req, res)); + // Dispatch httpServer.get("/query_region_list", (req, res) -> { // Log @@ -287,69 +300,15 @@ public final class DispatchServer { try { String body = req.ctx().body(); requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class); - } catch (Exception ignored) { - } + } 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", req.ip())); - Grasscutter.getLogger() - .info(String.format("[Dispatch] Client %s is trying to log in", req.ip())); - - // Login - Account account = DatabaseHelper.getAccountByName(requestData.account); - - // Check if account exists, else create a new one. - if (account == null) { - // 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); - - for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { - account.addPermission(permission); - } - - 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", - req.ip(), 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", req.ip())); - } - } else { - responseData.retcode = -201; - responseData.message = "Username not found."; - - Grasscutter.getLogger().info(String - .format("[Dispatch] Client %s failed to log in: Account no found", req.ip())); - } - } 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", req.ip(), - responseData.data.account.uid)); - } - - res.send(responseData); + res.send(authHandler.handleGameLogin(req, requestData)); }); // Login via token @@ -523,6 +482,29 @@ public final class DispatchServer { return result; } + public AuthenticationHandler getAuthHandler() { + if(authHandler == null) { + return new DefaultAuthenticationHandler(); + } + Grasscutter.getLogger().info(authHandler.getClass().getName()); + + return authHandler; + } + + public boolean registerAuthHandler(AuthenticationHandler authHandler) { + if(this.authHandler != null) { + Grasscutter.getLogger().error(String.format("[Dispatch] Unable to register '%s' authentication handler. \n" + + "The '%s' authentication handler has already been registered", authHandler.getClass().getName(), this.authHandler.getClass().getName())); + return false; + } + this.authHandler = authHandler; + return true; + } + + public void resetAuthHandler() { + this.authHandler = null; + } + public static class RegionData { QueryCurrRegionHttpRsp parsedRegionQuery; String Base64; diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java new file mode 100644 index 000000000..92a2961ea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java @@ -0,0 +1,16 @@ +package emu.grasscutter.server.dispatch.authentication; + +import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; +import emu.grasscutter.server.dispatch.json.LoginResultJson; +import express.http.Request; +import express.http.Response; + +public interface AuthenticationHandler { + + // This is in case plugins also want some sort of authentication + void handleLogin(Request req, Response res); + void handleRegister(Request req, Response res); + void handleChangePassword(Request req, Response res); + + LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData); +} diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java new file mode 100644 index 000000000..49459f509 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java @@ -0,0 +1,84 @@ +package emu.grasscutter.server.dispatch.authentication; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; +import emu.grasscutter.server.dispatch.json.LoginResultJson; +import express.http.Request; +import express.http.Response; + +public class DefaultAuthenticationHandler implements AuthenticationHandler { + + @Override + public void handleLogin(Request req, Response res) { + res.send("Authentication is not available with the default authentication method"); + } + + @Override + public void handleRegister(Request req, Response res) { + res.send("Authentication is not available with the default authentication method"); + } + + @Override + public void handleChangePassword(Request req, Response res) { + res.send("Authentication is not available with the default authentication method"); + } + + @Override + public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) { + LoginResultJson responseData = new LoginResultJson(); + + // Login + Account account = DatabaseHelper.getAccountByName(requestData.account); + + // Check if account exists, else create a new one. + if (account == null) { + // 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); + + for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { + account.addPermission(permission); + } + + 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", + req.ip(), 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", req.ip())); + } + } else { + responseData.retcode = -201; + responseData.message = "Username not found."; + + Grasscutter.getLogger().info(String + .format("[Dispatch] Client %s failed to log in: Account no found", req.ip())); + } + } 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", req.ip(), + responseData.data.account.uid)); + } + + return responseData; + } +} From e8aaee6515c23785e2b980e49b6a10db135abdde Mon Sep 17 00:00:00 2001 From: xtaodada Date: Mon, 2 May 2022 15:30:19 +0800 Subject: [PATCH 038/434] implement shopMail giftPackage function Co-authored-by: Kinesis --- .../java/emu/grasscutter/data/GameData.java | 5 ++++ .../data/common/RewardBoxItemData.java | 22 +++++++++++++++ .../grasscutter/data/def/RewardBoxData.java | 27 ++++++++++++++++++ .../grasscutter/data/def/ShopGoodsData.java | 5 ++++ .../grasscutter/game/inventory/GameItem.java | 13 ++++++++- .../game/managers/InventoryManager.java | 28 +++++++++++++++++++ .../emu/grasscutter/game/shop/ShopInfo.java | 10 +++++++ .../packet/recv/HandlerBuyGoodsReq.java | 5 ++-- 8 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/main/java/emu/grasscutter/data/common/RewardBoxItemData.java create mode 100644 src/main/java/emu/grasscutter/data/def/RewardBoxData.java diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index c187a1819..2ba251e71 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -62,6 +62,7 @@ public class GameData { private static final Int2ObjectMap fetterDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap fetterCharacterCardDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap rewardDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap rewardBoxDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap worldLevelDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap dailyDungeonDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap dungeonDataMap = new Int2ObjectOpenHashMap<>(); @@ -263,6 +264,10 @@ public class GameData { return rewardDataMap; } + public static Int2ObjectMap getRewardBoxDataMap() { + return rewardBoxDataMap; + } + public static Map> getFetterDataEntries() { if (fetters.isEmpty()) { fetterDataMap.forEach((k, v) -> { diff --git a/src/main/java/emu/grasscutter/data/common/RewardBoxItemData.java b/src/main/java/emu/grasscutter/data/common/RewardBoxItemData.java new file mode 100644 index 000000000..b32e2c650 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/common/RewardBoxItemData.java @@ -0,0 +1,22 @@ +package emu.grasscutter.data.common; + +public class RewardBoxItemData { + private int Id; + private String Count; + + public int getItemId() { + return Id; + } + + public void setItemId(int itemId) { + Id = itemId; + } + + public String getItemCount() { + return Count; + } + + public void setItemCount(String itemCount) { + Count = itemCount; + } +} diff --git a/src/main/java/emu/grasscutter/data/def/RewardBoxData.java b/src/main/java/emu/grasscutter/data/def/RewardBoxData.java new file mode 100644 index 000000000..c4c3ce527 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/RewardBoxData.java @@ -0,0 +1,27 @@ +package emu.grasscutter.data.def; + +import java.util.List; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.common.RewardBoxItemData; + +@ResourceType(name = "RewardPreviewExcelConfigData.json") +public class RewardBoxData extends GameResource { + public int Id; + public List PreviewItems; + + @Override + public int getId() { + return Id; + } + + public List getRewardBoxItemList() { + return PreviewItems; + } + + @Override + public void onLoad() { + + } +} diff --git a/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java b/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java index 1a4168637..a4d91e933 100644 --- a/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java +++ b/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java @@ -30,6 +30,7 @@ public class ShopGoodsData extends GameResource { private transient ShopInfo.ShopRefreshType RefreshTypeEnum; private int RefreshParam; + private int ShowId; @Override public void onLoad() { @@ -105,4 +106,8 @@ public class ShopGoodsData extends GameResource { public int getRefreshParam() { return RefreshParam; } + + public int getShowId() { + return ShowId; + } } diff --git a/src/main/java/emu/grasscutter/game/inventory/GameItem.java b/src/main/java/emu/grasscutter/game/inventory/GameItem.java index 7293b75c0..02ff8cd25 100644 --- a/src/main/java/emu/grasscutter/game/inventory/GameItem.java +++ b/src/main/java/emu/grasscutter/game/inventory/GameItem.java @@ -58,6 +58,9 @@ public class GameItem { // Relic private int mainPropId; private List appendPropIdList; + + // shopMailBox + private int rewardBoxId; private int equipCharacter; @Transient private int weaponEntityId; @@ -90,7 +93,7 @@ public class GameItem { // Equip data if (getItemType() == ItemType.ITEM_WEAPON) { - this.level = this.count > 1 ? this.count : 1; + this.level = Math.max(this.count, 1); this.affixes = new ArrayList<>(2); if (getItemData().getSkillAffix() != null) { for (int skillAffix : getItemData().getSkillAffix()) { @@ -248,6 +251,14 @@ public class GameItem { this.mainPropId = mainPropId; } + public int getRewardBoxId() { + return rewardBoxId; + } + + public void setRewardBoxId(int rewardBoxId) { + this.rewardBoxId = rewardBoxId; + } + public List getAppendPropIdList() { return appendPropIdList; } diff --git a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java index 2a9629b73..6fef8afc4 100644 --- a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java +++ b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java @@ -7,11 +7,13 @@ import java.util.stream.Collectors; import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.common.RewardBoxItemData; import emu.grasscutter.data.custom.OpenConfigEntry; import emu.grasscutter.data.custom.OpenConfigEntry.SkillPointModifier; import emu.grasscutter.data.def.AvatarPromoteData; import emu.grasscutter.data.def.AvatarSkillData; import emu.grasscutter.data.def.AvatarSkillDepotData; +import emu.grasscutter.data.def.RewardBoxData; import emu.grasscutter.data.def.WeaponPromoteData; import emu.grasscutter.data.def.AvatarSkillDepotData.InherentProudSkillOpens; import emu.grasscutter.data.def.AvatarTalentData; @@ -21,6 +23,7 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.inventory.MaterialType; import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; import emu.grasscutter.net.proto.MaterialInfoOuterClass.MaterialInfo; import emu.grasscutter.server.game.GameServer; @@ -897,6 +900,26 @@ public class InventoryManager { player.sendPacket(new PacketDestroyMaterialRsp(returnMaterialMap)); } + private boolean handleRewardBox(Player player, GameItem useItem) { + List rewardBoxDataList = GameData.getRewardBoxDataMap().values().stream().filter(x -> x.getId() == useItem.getRewardBoxId()).collect(Collectors.toList()); + if (rewardBoxDataList.isEmpty()) { + return false; + } + List rewardItemList = new ArrayList<>(); + for (RewardBoxItemData itemData : rewardBoxDataList.get(0).getRewardBoxItemList()) { + if (itemData.getItemId() == 0) { + continue; + } + String[] split = itemData.getItemCount().split(";"); + int itemCount = Integer.parseInt(split[(int) (Math.random()* split.length)]); + rewardItemList.add(new GameItem(itemData.getItemId(), itemCount)); + } + if (!rewardItemList.isEmpty()) { + player.getInventory().addItems(rewardItemList, ActionReason.Shop); + } + return true; + } + public GameItem useItem(Player player, long targetGuid, long itemGuid, int count) { Avatar target = player.getAvatars().getAvatarByGuid(targetGuid); GameItem useItem = player.getInventory().getItemByGuid(itemGuid); @@ -918,6 +941,11 @@ public class InventoryManager { used = player.getTeamManager().reviveAvatar(target) ? 1 : 0; } break; + case MATERIAL_CHEST: + if (useItem.getRewardBoxId() > 0) { + used = handleRewardBox(player, useItem) ? 1 : 0; + } + break; default: break; } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java index ebbeea2cd..ab5de27f6 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java @@ -13,6 +13,7 @@ public class ShopInfo { private List costItemList; private int boughtNum = 0; private int buyLimit = 0; + private int showId = 0; private int beginTime = 0; private int endTime = 1924992000; private int minLevel = 0; @@ -51,6 +52,7 @@ public class ShopInfo { this.mcoin = sgd.getCostMcoin(); this.hcoin = sgd.getCostHcoin(); this.buyLimit = sgd.getBuyLimit(); + this.showId = sgd.getShowId(); this.minLevel = sgd.getMinPlayerLevel(); this.maxLevel = sgd.getMaxPlayerLevel(); @@ -180,6 +182,14 @@ public class ShopInfo { this.maxLevel = maxLevel; } + public int getShowId() { + return showId; + } + + public void setShowId(int showId) { + this.showId = showId; + } + public ShopRefreshType getShopRefreshType() { if (refreshType == null) return ShopRefreshType.NONE; diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java index 0088b7176..7b773995d 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java @@ -4,7 +4,6 @@ import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.shop.ShopInfo; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.shop.ShopManager; @@ -19,7 +18,6 @@ import emu.grasscutter.server.packet.send.PacketBuyGoodsRsp; import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify; import emu.grasscutter.utils.Utils; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -92,6 +90,9 @@ public class HandlerBuyGoodsReq extends PacketHandler { session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum(), ShopManager.getShopNextRefreshTime(sg)); GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getId())); item.setCount(buyGoodsReq.getBoughtNum() * sg.getGoodsItem().getCount()); + if (sg.getShowId() > 0) { + item.setRewardBoxId(sg.getShowId()); + } session.getPlayer().getInventory().addItem(item, ActionReason.Shop, true); // fix: not notify when got virtual item from shop session.send(new PacketBuyGoodsRsp(buyGoodsReq.getShopType(), session.getPlayer().getGoodsLimit(sg.getGoodsId()).getHasBoughtInPeriod(), buyGoodsReq.getGoodsListList().stream().filter(x -> x.getGoodsId() == goodsId).findFirst().get())); } From 2661cc5ef391643795593bd1ac3e058babdacc0e Mon Sep 17 00:00:00 2001 From: BaiSugar Date: Mon, 2 May 2022 16:39:23 +0800 Subject: [PATCH 039/434] Fix announcement display --- data/GameAnnouncement.json | 29 +++++ data/GameAnnouncementList.json | 119 ++++++++++++++++++ .../server/dispatch/AnnouncementHandler.java | 35 ++++++ .../server/dispatch/DispatchServer.java | 4 +- 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 data/GameAnnouncement.json create mode 100644 data/GameAnnouncementList.json create mode 100644 src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java diff --git a/data/GameAnnouncement.json b/data/GameAnnouncement.json new file mode 100644 index 000000000..2bce06fdb --- /dev/null +++ b/data/GameAnnouncement.json @@ -0,0 +1,29 @@ +{ +"list": [ + { + "ann_id": 1, + "title": "Welcome to Grasscutter!", + "subtitle": "Welcome", + "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg", + "content": "Hi there!
First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! Check out our:
", + "lang": "es-es" + }, + { + "ann_id": 2, + "title": "How to use announcements", + "subtitle": "How to use", + "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg", + "content": "Tips
>How to use announcements

>Announcement content can use HTML

>The specific content of the announcement is stored in the program directorydata/GameAnnouncement.json, whileGameAnnouncementList.json stores the announcement list data

How to use
>In GameAnnouncement
ParametersDescription
ann_IdAnnouncement unique id
titleShow at the top of the content
subtitletitle shown on the left
bannerDisplay between content and title
contentas u see
langdisplay language
totalAnnouncement quantity


>In GameAnnouncementList
If you want to add an annouement, please add the list data in the announcement type corresponding to GameAnnouncementList, and finally add the announcement content in GameAnnouncement", + "lang": "es-es" + }, + { + "ann_id": 3, + "title": "ǻ--This is the event announcement", + "subtitle": "Welcome", + "banner":"https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg", + "content": "Welcome", + "lang": "es-es" + } +], +"total": 3 +} \ No newline at end of file diff --git a/data/GameAnnouncementList.json b/data/GameAnnouncementList.json new file mode 100644 index 000000000..ea3091fc7 --- /dev/null +++ b/data/GameAnnouncementList.json @@ -0,0 +1,119 @@ +{ + "t": "System.currentTimeMillis()", + "list": [ + { + "list": [ + { + "ann_id": 1, + "title": "Welcome to Grasscutter!", + "subtitle": "Welcome", + "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg", + "content": "", + "type_label": "Juego", + "tag_label": "1", + "tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png", + "login_alert": 1, + "lang": "es-es", + "start_time": "2020-09-25 04:05:30", + "end_time": "2023-10-30 11:00:00", + "type": 2, + "remind": 0, + "alert": 0, + "tag_start_time": "2000-01-02 15:04:05", + "tag_end_time": "2030-01-02 15:04:05", + "remind_ver": 1, + "has_content": true, + "extra_remind": 0 + }, + { + "ann_id": 2, + "title": "Ϸ -- This is the game announcement", + "subtitle": "This is the game announcement", + "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/85b7163c95745a76d49b3d163d893592_6487108933004985049.jpg", + "content": "", + "type_label": "Juego", + "tag_label": "1", + "tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png", + "login_alert": 1, + "lang": "es-es", + "start_time": "2020-09-25 15:12:09", + "end_time": "2030-10-30 11:00:00", + "type": 2, + "remind": 0, + "alert": 0, + "tag_start_time": "2000-01-02 08:04:05", + "tag_end_time": "2030-01-02 08:04:05", + "remind_ver": 1, + "has_content": true, + "extra_remind": 0 + } + ], + "type_id": 2, + "type_label": "Juego" + }, + { + "list": [ + { + "ann_id": 3, + "title": "ǻ--This is the event announcement", + "subtitle": "Welcome", + "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg", + "content": "", + "type_label": "Eventos", + "tag_label": "1", + "tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png", + "login_alert": 1, + "lang": "es-es", + "start_time": "2020-09-25 04:05:30", + "end_time": "2022-05-02 00:51:00", + "type": 2, + "remind": 0, + "alert": 0, + "tag_start_time": "2000-01-02 15:04:05", + "tag_end_time": "2022-05-02 00:51:00", + "remind_ver": 1, + "has_content": true, + "extra_remind": 0 + } + ], + "type_id": 1, + "type_label": "Eventos" + }, + { + "list": [ + {} + ], + "type_id": 3, + "type_label": "Others" + } + ], + "total": 3, + "type_list": [ + { + "id": 2, + "name": "Ϸϵͳ", + "mi18n_name": "Juego" + }, + { + "id": 1, + "name": "", + "mi18n_name": "Eventos" + }, + { + "id": 3, + "name": "", + "mi18n_name": "Others" + } + ], + "alert": true, + "alert_id": 2, + "timezone": -5, + "pic_list": [ + ], + "pic_total": 0, + "pic_type_list": [ + ], + "pic_alert": false, + "pic_alert_id": 0, + "static_sign": "" +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java new file mode 100644 index 000000000..333d8ea21 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java @@ -0,0 +1,35 @@ +package emu.grasscutter.server.dispatch; + +import emu.grasscutter.Grasscutter; +import express.http.HttpContextHandler; +import express.http.Request; +import express.http.Response; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Objects; + +public final class AnnouncementHandler implements HttpContextHandler { + @Override + public void handle(Request request, Response response) throws IOException {//event + if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(Grasscutter.getConfig().DATA_FOLDER + "GameAnnouncement.json")) +"}"); + } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { + String data = readToString(new File(Grasscutter.getConfig().DATA_FOLDER + "GameAnnouncementList.json")).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); + } + } + private static String readToString(File file) { + Long filelength = file.length(); + byte[] filecontent = new byte[filelength.intValue()]; + try { + FileInputStream in = new FileInputStream(file); + in.read(filecontent); + in.close(); + } catch (IOException fileNotFoundException) { + fileNotFoundException.printStackTrace(); + } + return new String(filecontent); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 5fdda7e87..79a103d17 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -408,9 +408,9 @@ public final class DispatchServer { // hk4e-api-os.hoyoverse.com httpServer.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"list\":[],\"total\":0,\"type_list\":[],\"alert\":false,\"alert_id\":0,\"timezone\":0,\"t\":\"" + System.currentTimeMillis() + "\"}}")); + httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new AnnouncementHandler()); // hk4e-api-os-static.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"list\":[],\"total\":0}}")); + httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new AnnouncementHandler()); // hk4e-sdk-os.hoyoverse.com httpServer.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); From d912b59d93b09ac67bb8c70dd6301bd9da688784 Mon Sep 17 00:00:00 2001 From: mingjun97 Date: Sun, 1 May 2022 23:17:18 -0700 Subject: [PATCH 040/434] Utils for gacha history record subsystem * Auto generate mapping files with command `java -jar grasscutter.jar -gachamap` * Static file provider * For gacha record webpage * All static files should be stored at `GRASSCUTTER_RESOURCE/gcstatic/` * Can benefit other subsystem in future when webpages involved --- README.md | 2 + README_zh-CN.md | 2 + data/gacha_records.html | 16 +++- .../java/emu/grasscutter/Grasscutter.java | 3 + .../server/dispatch/DispatchServer.java | 5 + .../http/gcstatic/StaticFileHandler.java | 31 +++++++ .../java/emu/grasscutter/tools/Tools.java | 93 +++++++++++++++++++ 7 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/http/gcstatic/StaticFileHandler.java diff --git a/README.md b/README.md index dc87ff1d8..51d360628 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ You can find the output jar in the root of the project folder. You might want to use this command (`java -jar grasscutter.jar -handbook`) in a cmd that is in the grasscutter folder. It will create a handbook file (GM Handbook.txt) where you can find the item IDs for stuff you want +You may want to use this command (`java -jar grasscutter.jar -gachamap`) to generate a mapping file for the gacha record subsystem. The file will be generated to `GRASSCUTTER_RESOURCE/gcstatic` folder. Otherwise you may only see number IDs in the gacha record page. + 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. to run commands ingame, you need to add prefix `/` or `!` such as `/pos` | Commands | Usage | Permission node | Availability | description | Alias | diff --git a/README_zh-CN.md b/README_zh-CN.md index 9a58b9565..4ad7e6ff1 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -102,6 +102,8 @@ chmod +x gradlew 你可能需要在终端中运行 `java -jar grasscutter.jar -handbook` 它将会创建一个 `GM Handbook.txt` 以方便您查阅物品ID等 +你可能需要在终端中运行 `java -jar grasscutter.jar -gachamap` 来使得祈愿历史记录系统正常显示物品信息。 这个命令生成一个配置文件到如下文件夹:`GRASSCUTTER_RESOURCE/gcstatic`。 不执行此命令,您的祈愿历史记录中将只会显示数字ID而非物品名称。(目前仅支持自动生成英文记录信息) + 在每个玩家的朋友列表中都有一个名为“Server”的虚拟用户,你可以通过发送消息来使用命令。命令也适用于其他聊天室,例如私人/团队聊天。 要在游戏中使用命令,需要添加 `/` 或 `!` 前缀,如 `/pos` diff --git a/data/gacha_records.html b/data/gacha_records.html index 21270a046..55240e77d 100644 --- a/data/gacha_records.html +++ b/data/gacha_records.html @@ -1,5 +1,7 @@ + + + + + + + + Gacha Records - - - +
-

Gacha Records

-

-
-
- - +
+

Gacha Records

+
+ - - + + + +
TimeItemDateItem
-
-
+ - \ No newline at end of file + From ee0a246471013e7303c0d494523372eb08fc27d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=93=9D=E4=BA=91Reyes?= Date: Mon, 2 May 2022 19:51:09 +0800 Subject: [PATCH 044/434] Update gacha_records.html --- data/gacha_records.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/data/gacha_records.html b/data/gacha_records.html index ae1e6044e..7adc08f29 100644 --- a/data/gacha_records.html +++ b/data/gacha_records.html @@ -39,6 +39,15 @@ font-weight:300; border:none; } + .yellow { + color: yellow; + } + .blue { + color: rgb(75, 107, 251); + } + .purple { + color: rgb(242, 40, 242); + } Gacha Records + diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index f322e5fdf..a1c8a5c7c 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -76,7 +76,7 @@ public final class Grasscutter { Tools.createGmHandbook(); return; } case "-gachamap" -> { - Tools.createGachaMapping(); return; + Tools.createGachaMapping("./gacha_mappings.js"); return; } } } diff --git a/src/main/java/emu/grasscutter/Language.java b/src/main/java/emu/grasscutter/Language.java new file mode 100644 index 000000000..e5150a6fe --- /dev/null +++ b/src/main/java/emu/grasscutter/Language.java @@ -0,0 +1,285 @@ +package emu.grasscutter; + +public final class Language { + public String An_error_occurred_during_game_update = "An error occurred during game update."; + public String Starting_Grasscutter = "Starting Grasscutter..."; + public String Invalid_server_run_mode = "Invalid server run mode."; + public String Server_run_mode = "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter..."; + public String Shutting_down = "Shutting down..."; + public String Start_done = "Done! For help, type \"help\""; + public String Dispatch_mode_not_support_command = "Commands are not supported in dispatch only mode."; + public String Command_error = "Command error:"; + public String error = "An error occurred."; + public String grasscutter_is_free = "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter"; + public String Game_start_port = "Game Server started on port %s"; + public String Client_connect = "Client connected from %s"; + public String Client_disconnect = "Client disconnected from %s"; + public String Client_request = "[Dispatch] Client %s %s request: %s"; + public String Not_load_keystore = "[Dispatch] Unable to load keystore. Trying default keystore password..."; + public String Use_default_keystore = "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."; + public String Load_keystore_error = "[Dispatch] Error while loading keystore!"; + public String Not_find_ssl_cert = "[Dispatch] No SSL cert found! Falling back to HTTP server."; + public String Welcome = "Welcome to Grasscutter"; + public String Potential_unhandled_request = "[Dispatch] Potential unhandled %s request: %s"; + public String Client_login_token = "[Dispatch] Client %s is trying to log in via token"; + public String Client_token_login_failed = "[Dispatch] Client %s failed to log in via token"; + public String Client_login_in_token = "[Dispatch] Client %s logged in via token as %s"; + public String Game_account_cache_error = "Game account cache information error"; + public String Wrong_session_key = "Wrong session key."; + public String Client_exchange_combo_token = "[Dispatch] Client %s succeed to exchange combo token"; + public String Client_failed_exchange_combo_token = "[Dispatch] Client %s failed to exchange combo token"; + public String Dispatch_start_server_port = "[Dispatch] Dispatch server started on port %s"; + public String Client_failed_login_account_create = "[Dispatch] Client %s failed to log in: Account %s created"; + public String Client_failed_login_account_create_failed = "[Dispatch] Client %s failed to log in: Account create failed"; + public String Client_failed_login_account_no_found = "[Dispatch] Client %s failed to log in: Account no found"; + public String Client_login = "[Dispatch] Client %s logged in as %s"; + public String Username_not_found = "Username not found."; + public String Username_not_found_create_failed = "Username not found, create failed."; + + // Command + public String No_command_specified = "No command specified."; + public String Unknown_command = "Unknown command: "; + public String You_not_permission_run_command = "You do not have permission to run this command."; + public String This_command_can_only_run_from_console = "This command can only be run from the console."; + public String Run_this_command_in_game = "Run this command in-game."; + public String Invalid_playerId = "Invalid playerId."; + public String Player_not_found = "Player not found."; + public String Player_is_offline = "Player is offline."; + public String Invalid_item_id = "Invalid item id."; + public String Invalid_item_or_player_id = "Invalid item or player ID."; + public String Enabled = "enabled"; + public String Disabled = "disabled"; + public String No_command_found = "No command found."; + public String Help = "Help"; + public String Player_not_found_or_offline = "Player not found or offline."; + public String Invalid_arguments = "Invalid arguments."; + public String Success = "Success"; + public String Invalid_entity_id = "Invalid entity id."; + + // Help + public String Help_usage = " Usage: "; + public String Help_aliases = " Aliases: "; + public String Help_available_command = "Available commands:"; + + // Account + public String Modify_user_account = "Modify user accounts"; + public String Invalid_UID = "Invalid UID."; + public String Account_exists = "Account already exists."; + public String Account_create_UID = "Account created with UID %s."; + public String Account_delete = "Account deleted."; + public String Account_not_find = "Account not found."; + public String Account_command_usage = "Usage: account [uid]"; + + // Broadcast + public String Broadcast_command_usage = "Usage: broadcast "; + public String Broadcast_message_sent = "Message sent."; + + // ChangeScene + public String Change_screen_usage = "Usage: changescene "; + public String Change_screen_you_in_that_screen = "You are already in that scene"; + public String Change_screen = "Changed to scene "; + public String Change_screen_not_exist = "Scene does not exist"; + + // Clear + public String Clear_weapons = "Cleared weapons for %s ."; + public String Clear_artifacts = "Cleared artifacts for %s ."; + public String Clear_materials = "Cleared materials for %s ."; + public String Clear_furniture = "Cleared furniture for %s ."; + public String Clear_displays = "Cleared displays for %s ."; + public String Clear_virtuals = "Cleared virtuals for %s ."; + public String Clear_everything = "Cleared everything for %s ."; + + // Coop + public String Coop_usage = "Usage: coop "; + + // Drop + public String Drop_usage = "Usage: drop [amount]"; + public String Drop_dropped_of = "Dropped %s of %s."; + + // EnterDungeon + public String EnterDungeon_usage = "Usage: enterdungeon "; + public String EnterDungeon_changed_to_dungeon = "Changed to dungeon "; + public String EnterDungeon_dungeon_not_found = "Dungeon does not exist"; + public String EnterDungeon_you_in_that_dungeon = "You are already in that dungeon"; + + // GiveAll + public String GiveAll_usage = "Usage: giveall [player] [amount]"; + public String GiveAll_item = "Giving all items..."; + public String GiveAll_done = "Giving all items done"; + public String GiveAll_invalid_amount_or_playerId = "Invalid amount or player ID."; + + // GiveArtifact + public String GiveArtifact_usage = "Usage: giveart|gart [player] [[,]]... [level]"; + public String GiveArtifact_invalid_artifact_id = "Invalid artifact ID."; + public String GiveArtifact_given = "Given %s to %s."; + + // GiveChar + public String GiveChar_usage = "Usage: givechar [amount]"; + public String GiveChar_given = "Given %s with level %s to %s."; + public String GiveChar_invalid_avatar_id = "Invalid avatar id."; + public String GiveChar_invalid_avatar_level = "Invalid avatar level."; + public String GiveChar_invalid_avatar_or_player_id = "Invalid avatar or player ID."; + + // Give + public String Give_usage = "Usage: give [amount] [level]"; + public String Give_refinement_only_applicable_weapons = "Refinement is only applicable to weapons."; + public String Give_refinement_must_between_1_and_5 = "Refinement must be between 1 and 5."; + public String Give_given = "Given %s of %s to %s."; + public String Give_given_with_level_and_refinement = "Given %s with level %s, refinement %s %s times to %s"; + public String Give_given_level = "Given %s with level %s %s times to %s"; + + // GodMode + public String Godmode_status = "Godmode is now %s for %s ."; + + // Heal + public String Heal_message = "All characters have been healed."; + + // Kick + public String Kick_player_kick_player = "Player [%s:%s] has kicked player [%s:%s]"; + public String Kick_server_player = "Kicking player [%s:%s]"; + + // Kill + public String Kill_usage = "Usage: killall [playerUid] [sceneId]"; + public String Kill_scene_not_found_in_player_world = "Scene not found in player world"; + public String Kill_kill_monsters_in_scene = "Killing %s monsters in scene %s"; + + // KillCharacter + public String KillCharacter_usage = "Usage: /killcharacter [playerId]"; + public String KillCharacter_kill_current_character = "Killed %s current character."; + + // List + public String List_message = "There are %s player(s) online:"; + + // Permission + public String Permission_usage = "Usage: permission "; + public String Permission_add = "Permission added."; + public String Permission_have_permission = "They already have this permission!"; + public String Permission_remove = "Permission removed."; + public String Permission_not_have_permission = "They don't have this permission!"; + + // Position + public String Position_message = "Coord: %.3f, %.3f, %.3f\nScene id: %d"; + + // Reload + public String Reload_reload_start = "Reloading config."; + public String Reload_reload_done = "Reload complete."; + + // ResetConst + public String ResetConst_reset_all = "Reset all avatars' constellations."; + public String ResetConst_reset_all_done = "Constellations for %s have been reset. Please relog to see changes."; + + // ResetShopLimit + public String ResetShopLimit_usage = "Usage: /resetshop "; + + // SendMail + public String SendMail_usage = "Usage: give [player] [amount]"; + public String SendMail_user_not_exist = "The user with an id of '%s' does not exist"; + public String SendMail_start_composition = "Starting composition of message.\nPlease use `/sendmail ` to continue.\nYou can use `/sendmail stop` at any time"; + public String SendMail_templates = "Mail templates coming soon implemented..."; + public String SendMail_invalid_arguments = "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`"; + public String SendMail_send_cancel = "Message sending cancelled"; + public String SendMail_send_done = "Message sent to user %s!"; + public String SendMail_send_all_done = "Message sent to all users!"; + public String SendMail_not_composition_end = "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel"; + public String SendMail_Please_use = "Please use `/sendmail %s`"; + public String SendMail_set_title = "Message title set as '%s'.\nUse '/sendmail <content>' to continue."; + public String SendMail_set_contents = "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue."; + public String SendMail_set_message_sender = "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue."; + public String SendMail_send = "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message."; + public String SendMail_invalid_arguments_please_use = "Invalid arguments \n Please use `/sendmail %s`"; + public String SendMail_title = "<title>"; + public String SendMail_message = "<message>"; + public String SendMail_sender = "<sender>"; + public String SendMail_arguments = "<itemId|itemName|finish> [amount] [level]"; + public String SendMail_error = "ERROR: invalid construction stage %s. Check console for stacktrace."; + + // SendMessage + public String SendMessage_usage = "Usage: sendmessage <player> <message>"; + public String SenaMessage_message_sent = "Message sent."; + + // SetFetterLevel + public String SetFetterLevel_usage = "Usage: setfetterlevel <level>"; + public String SetFetterLevel_fetter_level_must_between_0_and_10 = "Fetter level must be between 0 and 10."; + public String SetFetterLevel_fetter_set_level = "Fetter level set to %s"; + public String SetFetterLevel_invalid_fetter_level = "Invalid fetter level."; + + // SetStats + public String SetStats_usage = "Usage: setstats|stats <stat> <value>"; + public String SetStats_setstats_help_message = "Usage: /setstats|stats <hp | mhp | def | atk | em | er | crate | cdmg> <value> for basic stats"; + public String SetStats_stats_help_message = "Usage: /stats <epyro | ecryo | ehydro | egeo | edend | eelec | ephys> <amount> for elemental bonus"; + public String SetStats_set_max_hp = "MAX HP set to %s."; + public String SetStats_set_max_hp_error = "Invalid Max HP value."; + public String SetStats_set_hp = "HP set to %s."; + public String SetStats_set_hp_error = "Invalid HP value."; + public String SetStats_set_def = "DEF set to %s."; + public String SetStats_set_def_error = "Invalid DEF value."; + public String SetStats_set_atk = "ATK set to %s."; + public String SetStats_set_atk_error = "Invalid ATK value."; + public String SetStats_set_em = "Elemental Mastery set to %s."; + public String SetStats_set_em_error = "Invalid EM value."; + public String SetStats_set_er = "Energy recharge set to %s%."; + public String SetStats_set_er_error = "Invalid ER value."; + public String SetStats_set_cr = "Crit Rate set to %s%."; + public String SetStats_set_cr_error = "Invalid Crit Rate value."; + public String SetStats_set_cd = "Crit DMG set to %s%."; + public String SetStats_set_cd_error = "Invalid Crit DMG value."; + public String SetStats_set_pdb = "Pyro DMG Bonus set to %s%."; + public String SetStats_set_pdb_error = "Invalid Pyro DMG Bonus value."; + public String SetStats_set_cdb = "Cyro DMG Bonus set to %s%."; + public String SetStats_set_cdb_error = "Invalid Cyro DMG Bonus value."; + public String SetStats_set_hdb = "Hydro DMG Bonus set to %s%."; + public String SetStats_set_hdb_error = "Invalid Hydro DMG Bonus value."; + public String SetStats_set_adb = "Anemo DMG Bonus set to %s%."; + public String SetStats_set_adb_error = "Invalid Anemo DMG Bonus value."; + public String SetStats_set_gdb = "Geo DMG Bonus set to %s%."; + public String SetStats_set_gdb_error = "Invalid Geo DMG Bonus value."; + public String SetStats_set_edb = "Electro DMG Bonus set to %s%."; + public String SetStats_set_edb_error = "Invalid Electro DMG Bonus value."; + public String SetStats_set_physdb = "Physical DMG Bonus set to %s%."; + public String SetStats_set_physdb_error = "Invalid Physical DMG Bonus value."; + public String SetStats_set_ddb = "Dendro DMG Bonus set to %s%."; + public String SetStats_set_ddb_error = "Invalid Dendro DMG Bonus value."; + + // SetWorldLevel + public String SetWorldLevel_usage = "Usage: setworldlevel <level>"; + public String SetWorldLevel_world_level_must_between_0_and_8 = "World level must be between 0-8"; + public String SetWorldLevel_set_world_level = "World level set to %s."; + public String SetWorldLevel_invalid_world_level = "Invalid world level."; + + // Spawn + public String Spawn_usage = "Usage: spawn <entityId> [amount] [level(monster only)]"; + public String Spawn_message = "Spawned %s of %s."; + + // Stop + public String Stop_message = "Server shutting down..."; + + // Talent + public String Talent_usage_1 = "To set talent level: /talent set <talentID> <value>"; + public String Talent_usage_2 = "Another way to set talent level: /talent <n or e or q> <value>"; + public String Talent_usage_3 = "To get talent ID: /talent getid"; + public String Talent_lower_16 = "Invalid talent level. Level should be lower than 16"; + public String Talent_set_atk = "Set talent Normal ATK to %s."; + public String Talent_set_e = "Set talent E to %s."; + public String Talent_set_q = "Set talent Q to %s."; + public String Talent_invalid_skill_id = "Invalid skill ID."; + public String Talent_set_this = "Set this talent to %s."; + public String Talent_invalid_talent_level = "Invalid talent level."; + public String Talent_normal_attack_id = "Normal Attack ID %s."; + public String Talent_e_skill_id = "E skill ID %s."; + public String Talent_q_skill_id = "Q skill ID %s."; + + // TeleportAll + public String TeleportAll_message = "You only can use this command in MP mode."; + + // Teleport + public String Teleport_usage_server = "Usage: /tp @<player id> <x> <y> <z> [scene id]"; + public String Teleport_usage = "Usage: /tp [@<player id>] <x> <y> <z> [scene id]"; + public String Teleport_specify_player_id = "You must specify a player id."; + public String Teleport_invalid_position = "Invalid position."; + public String Teleport_message = "Teleported %s to %s,%s,%s in scene %s"; + + // Weather + public String Weather_usage = "Usage: weather <weatherId> [climateId]"; + public String Weather_message = "Changed weather to %s with climate %s"; + public String Weather_invalid_id = "Invalid ID."; +} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 196faf880..85f02a36d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -16,14 +16,16 @@ import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler; import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; +import emu.grasscutter.server.dispatch.http.GachaRecordHandler; import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; -import emu.grasscutter.server.http.gacha.GachaRecordHandler; -import emu.grasscutter.server.http.gcstatic.StaticFileHandler; +import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; import express.Express; +import io.javalin.http.staticfiles.Location; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -442,11 +444,18 @@ public final class DispatchServer { // webstatic-sea.hoyoverse.com httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); - // gacha record + // gacha record. + String gachaMappingsPath = Utils.toFilePath(Grasscutter.getConfig().DATA_FOLDER + "/gacha_mappings.js"); + // TODO: Only serve the html page and have a subsequent request to fetch the gacha data. httpServer.get("/gacha", new GachaRecordHandler()); + if(!(new File(gachaMappingsPath).exists())) { + Tools.createGachaMapping(gachaMappingsPath); + } - // static file provider - httpServer.get("/gcstatic/*", new StaticFileHandler()); + httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL); + + // static file support for plugins + httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); Grasscutter.getLogger().info(Grasscutter.getLanguage().Dispatch_start_server_port.replace("{port}", Integer.toString(httpServer.raw().port()))); diff --git a/src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java similarity index 88% rename from src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java rename to src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java index 0798a150f..8ca47976e 100644 --- a/src/main/java/emu/grasscutter/server/http/gacha/GachaRecordHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java @@ -1,4 +1,4 @@ -package emu.grasscutter.server.http.gacha; +package emu.grasscutter.server.dispatch.http; import java.io.File; import java.io.IOException; @@ -7,6 +7,7 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; import express.http.HttpContextHandler; import express.http.Request; import express.http.Response; @@ -14,7 +15,7 @@ import express.http.Response; public final class GachaRecordHandler implements HttpContextHandler { String render_template; public GachaRecordHandler() { - File template = new File(Grasscutter.getConfig().DATA_FOLDER + "gacha_records.html"); + File template = new File(Utils.toFilePath(Grasscutter.getConfig().DATA_FOLDER + "/gacha_records.html")); if (template.exists()) { // Load from cache render_template = new String(FileUtils.read(template)); diff --git a/src/main/java/emu/grasscutter/server/http/gcstatic/StaticFileHandler.java b/src/main/java/emu/grasscutter/server/http/gcstatic/StaticFileHandler.java deleted file mode 100644 index 0cdccb2c1..000000000 --- a/src/main/java/emu/grasscutter/server/http/gcstatic/StaticFileHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package emu.grasscutter.server.http.gcstatic; - -import java.io.File; -import java.io.IOException; - -import emu.grasscutter.Grasscutter; -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -public final class StaticFileHandler implements HttpContextHandler { - String static_folder; - public StaticFileHandler() { - static_folder = Grasscutter.getConfig().RESOURCE_FOLDER + "/gcstatic"; - } - - @Override - public void handle(Request req, Response res) throws IOException { - // Grasscutter.getLogger().info( req.path()); - - String reqFilename = req.path().replace("/gcstatic", ""); // remove the leading path - reqFilename = reqFilename.replace("/../", "/./"); // security guard to prevent arbitrary read - File resFile = new File(static_folder + reqFilename); - if (resFile.exists()) { - res.sendFile(resFile.toPath()); - } else { - res.status(404); - res.send("404"); - } - } -} diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index d7f5e986a..c4ccd2d2e 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -96,7 +96,7 @@ public final class Tools { } @SuppressWarnings("deprecation") - public static void createGachaMapping() throws Exception { + public static void createGachaMapping(String location) throws Exception { ResourceLoader.loadResources(); Map<Long, String> map; @@ -106,11 +106,7 @@ public final class Tools { List<Integer> list; - - String fileName = Grasscutter.getConfig().RESOURCE_FOLDER + "/gcstatic"; - File folder = new File(fileName); - if (!folder.exists()) { folder.mkdirs(); } // create folder if it doesn't exist - fileName = fileName + "/mappings.js"; + String fileName = location; try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) { From a7273fd84a7c1f0b09c3d4b735a349c4ae9f28f4 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon <benjamin7006@gmail.com> Date: Thu, 5 May 2022 19:20:18 +0800 Subject: [PATCH 106/434] I honestly don't know what happened.... --- src/main/java/emu/grasscutter/Language.java | 285 -------------------- 1 file changed, 285 deletions(-) delete mode 100644 src/main/java/emu/grasscutter/Language.java diff --git a/src/main/java/emu/grasscutter/Language.java b/src/main/java/emu/grasscutter/Language.java deleted file mode 100644 index e5150a6fe..000000000 --- a/src/main/java/emu/grasscutter/Language.java +++ /dev/null @@ -1,285 +0,0 @@ -package emu.grasscutter; - -public final class Language { - public String An_error_occurred_during_game_update = "An error occurred during game update."; - public String Starting_Grasscutter = "Starting Grasscutter..."; - public String Invalid_server_run_mode = "Invalid server run mode."; - public String Server_run_mode = "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter..."; - public String Shutting_down = "Shutting down..."; - public String Start_done = "Done! For help, type \"help\""; - public String Dispatch_mode_not_support_command = "Commands are not supported in dispatch only mode."; - public String Command_error = "Command error:"; - public String error = "An error occurred."; - public String grasscutter_is_free = "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter"; - public String Game_start_port = "Game Server started on port %s"; - public String Client_connect = "Client connected from %s"; - public String Client_disconnect = "Client disconnected from %s"; - public String Client_request = "[Dispatch] Client %s %s request: %s"; - public String Not_load_keystore = "[Dispatch] Unable to load keystore. Trying default keystore password..."; - public String Use_default_keystore = "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."; - public String Load_keystore_error = "[Dispatch] Error while loading keystore!"; - public String Not_find_ssl_cert = "[Dispatch] No SSL cert found! Falling back to HTTP server."; - public String Welcome = "Welcome to Grasscutter"; - public String Potential_unhandled_request = "[Dispatch] Potential unhandled %s request: %s"; - public String Client_login_token = "[Dispatch] Client %s is trying to log in via token"; - public String Client_token_login_failed = "[Dispatch] Client %s failed to log in via token"; - public String Client_login_in_token = "[Dispatch] Client %s logged in via token as %s"; - public String Game_account_cache_error = "Game account cache information error"; - public String Wrong_session_key = "Wrong session key."; - public String Client_exchange_combo_token = "[Dispatch] Client %s succeed to exchange combo token"; - public String Client_failed_exchange_combo_token = "[Dispatch] Client %s failed to exchange combo token"; - public String Dispatch_start_server_port = "[Dispatch] Dispatch server started on port %s"; - public String Client_failed_login_account_create = "[Dispatch] Client %s failed to log in: Account %s created"; - public String Client_failed_login_account_create_failed = "[Dispatch] Client %s failed to log in: Account create failed"; - public String Client_failed_login_account_no_found = "[Dispatch] Client %s failed to log in: Account no found"; - public String Client_login = "[Dispatch] Client %s logged in as %s"; - public String Username_not_found = "Username not found."; - public String Username_not_found_create_failed = "Username not found, create failed."; - - // Command - public String No_command_specified = "No command specified."; - public String Unknown_command = "Unknown command: "; - public String You_not_permission_run_command = "You do not have permission to run this command."; - public String This_command_can_only_run_from_console = "This command can only be run from the console."; - public String Run_this_command_in_game = "Run this command in-game."; - public String Invalid_playerId = "Invalid playerId."; - public String Player_not_found = "Player not found."; - public String Player_is_offline = "Player is offline."; - public String Invalid_item_id = "Invalid item id."; - public String Invalid_item_or_player_id = "Invalid item or player ID."; - public String Enabled = "enabled"; - public String Disabled = "disabled"; - public String No_command_found = "No command found."; - public String Help = "Help"; - public String Player_not_found_or_offline = "Player not found or offline."; - public String Invalid_arguments = "Invalid arguments."; - public String Success = "Success"; - public String Invalid_entity_id = "Invalid entity id."; - - // Help - public String Help_usage = " Usage: "; - public String Help_aliases = " Aliases: "; - public String Help_available_command = "Available commands:"; - - // Account - public String Modify_user_account = "Modify user accounts"; - public String Invalid_UID = "Invalid UID."; - public String Account_exists = "Account already exists."; - public String Account_create_UID = "Account created with UID %s."; - public String Account_delete = "Account deleted."; - public String Account_not_find = "Account not found."; - public String Account_command_usage = "Usage: account <create|delete> <username> [uid]"; - - // Broadcast - public String Broadcast_command_usage = "Usage: broadcast <message>"; - public String Broadcast_message_sent = "Message sent."; - - // ChangeScene - public String Change_screen_usage = "Usage: changescene <scene id>"; - public String Change_screen_you_in_that_screen = "You are already in that scene"; - public String Change_screen = "Changed to scene "; - public String Change_screen_not_exist = "Scene does not exist"; - - // Clear - public String Clear_weapons = "Cleared weapons for %s ."; - public String Clear_artifacts = "Cleared artifacts for %s ."; - public String Clear_materials = "Cleared materials for %s ."; - public String Clear_furniture = "Cleared furniture for %s ."; - public String Clear_displays = "Cleared displays for %s ."; - public String Clear_virtuals = "Cleared virtuals for %s ."; - public String Clear_everything = "Cleared everything for %s ."; - - // Coop - public String Coop_usage = "Usage: coop <playerId> <target playerId>"; - - // Drop - public String Drop_usage = "Usage: drop <itemId|itemName> [amount]"; - public String Drop_dropped_of = "Dropped %s of %s."; - - // EnterDungeon - public String EnterDungeon_usage = "Usage: enterdungeon <dungeon id>"; - public String EnterDungeon_changed_to_dungeon = "Changed to dungeon "; - public String EnterDungeon_dungeon_not_found = "Dungeon does not exist"; - public String EnterDungeon_you_in_that_dungeon = "You are already in that dungeon"; - - // GiveAll - public String GiveAll_usage = "Usage: giveall [player] [amount]"; - public String GiveAll_item = "Giving all items..."; - public String GiveAll_done = "Giving all items done"; - public String GiveAll_invalid_amount_or_playerId = "Invalid amount or player ID."; - - // GiveArtifact - public String GiveArtifact_usage = "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]"; - public String GiveArtifact_invalid_artifact_id = "Invalid artifact ID."; - public String GiveArtifact_given = "Given %s to %s."; - - // GiveChar - public String GiveChar_usage = "Usage: givechar <player> <itemId|itemName> [amount]"; - public String GiveChar_given = "Given %s with level %s to %s."; - public String GiveChar_invalid_avatar_id = "Invalid avatar id."; - public String GiveChar_invalid_avatar_level = "Invalid avatar level."; - public String GiveChar_invalid_avatar_or_player_id = "Invalid avatar or player ID."; - - // Give - public String Give_usage = "Usage: give <player> <itemId|itemName> [amount] [level]"; - public String Give_refinement_only_applicable_weapons = "Refinement is only applicable to weapons."; - public String Give_refinement_must_between_1_and_5 = "Refinement must be between 1 and 5."; - public String Give_given = "Given %s of %s to %s."; - public String Give_given_with_level_and_refinement = "Given %s with level %s, refinement %s %s times to %s"; - public String Give_given_level = "Given %s with level %s %s times to %s"; - - // GodMode - public String Godmode_status = "Godmode is now %s for %s ."; - - // Heal - public String Heal_message = "All characters have been healed."; - - // Kick - public String Kick_player_kick_player = "Player [%s:%s] has kicked player [%s:%s]"; - public String Kick_server_player = "Kicking player [%s:%s]"; - - // Kill - public String Kill_usage = "Usage: killall [playerUid] [sceneId]"; - public String Kill_scene_not_found_in_player_world = "Scene not found in player world"; - public String Kill_kill_monsters_in_scene = "Killing %s monsters in scene %s"; - - // KillCharacter - public String KillCharacter_usage = "Usage: /killcharacter [playerId]"; - public String KillCharacter_kill_current_character = "Killed %s current character."; - - // List - public String List_message = "There are %s player(s) online:"; - - // Permission - public String Permission_usage = "Usage: permission <add|remove> <username> <permission>"; - public String Permission_add = "Permission added."; - public String Permission_have_permission = "They already have this permission!"; - public String Permission_remove = "Permission removed."; - public String Permission_not_have_permission = "They don't have this permission!"; - - // Position - public String Position_message = "Coord: %.3f, %.3f, %.3f\nScene id: %d"; - - // Reload - public String Reload_reload_start = "Reloading config."; - public String Reload_reload_done = "Reload complete."; - - // ResetConst - public String ResetConst_reset_all = "Reset all avatars' constellations."; - public String ResetConst_reset_all_done = "Constellations for %s have been reset. Please relog to see changes."; - - // ResetShopLimit - public String ResetShopLimit_usage = "Usage: /resetshop <player id>"; - - // SendMail - public String SendMail_usage = "Usage: give [player] <itemId|itemName> [amount]"; - public String SendMail_user_not_exist = "The user with an id of '%s' does not exist"; - public String SendMail_start_composition = "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time"; - public String SendMail_templates = "Mail templates coming soon implemented..."; - public String SendMail_invalid_arguments = "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`"; - public String SendMail_send_cancel = "Message sending cancelled"; - public String SendMail_send_done = "Message sent to user %s!"; - public String SendMail_send_all_done = "Message sent to all users!"; - public String SendMail_not_composition_end = "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel"; - public String SendMail_Please_use = "Please use `/sendmail %s`"; - public String SendMail_set_title = "Message title set as '%s'.\nUse '/sendmail <content>' to continue."; - public String SendMail_set_contents = "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue."; - public String SendMail_set_message_sender = "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue."; - public String SendMail_send = "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message."; - public String SendMail_invalid_arguments_please_use = "Invalid arguments \n Please use `/sendmail %s`"; - public String SendMail_title = "<title>"; - public String SendMail_message = "<message>"; - public String SendMail_sender = "<sender>"; - public String SendMail_arguments = "<itemId|itemName|finish> [amount] [level]"; - public String SendMail_error = "ERROR: invalid construction stage %s. Check console for stacktrace."; - - // SendMessage - public String SendMessage_usage = "Usage: sendmessage <player> <message>"; - public String SenaMessage_message_sent = "Message sent."; - - // SetFetterLevel - public String SetFetterLevel_usage = "Usage: setfetterlevel <level>"; - public String SetFetterLevel_fetter_level_must_between_0_and_10 = "Fetter level must be between 0 and 10."; - public String SetFetterLevel_fetter_set_level = "Fetter level set to %s"; - public String SetFetterLevel_invalid_fetter_level = "Invalid fetter level."; - - // SetStats - public String SetStats_usage = "Usage: setstats|stats <stat> <value>"; - public String SetStats_setstats_help_message = "Usage: /setstats|stats <hp | mhp | def | atk | em | er | crate | cdmg> <value> for basic stats"; - public String SetStats_stats_help_message = "Usage: /stats <epyro | ecryo | ehydro | egeo | edend | eelec | ephys> <amount> for elemental bonus"; - public String SetStats_set_max_hp = "MAX HP set to %s."; - public String SetStats_set_max_hp_error = "Invalid Max HP value."; - public String SetStats_set_hp = "HP set to %s."; - public String SetStats_set_hp_error = "Invalid HP value."; - public String SetStats_set_def = "DEF set to %s."; - public String SetStats_set_def_error = "Invalid DEF value."; - public String SetStats_set_atk = "ATK set to %s."; - public String SetStats_set_atk_error = "Invalid ATK value."; - public String SetStats_set_em = "Elemental Mastery set to %s."; - public String SetStats_set_em_error = "Invalid EM value."; - public String SetStats_set_er = "Energy recharge set to %s%."; - public String SetStats_set_er_error = "Invalid ER value."; - public String SetStats_set_cr = "Crit Rate set to %s%."; - public String SetStats_set_cr_error = "Invalid Crit Rate value."; - public String SetStats_set_cd = "Crit DMG set to %s%."; - public String SetStats_set_cd_error = "Invalid Crit DMG value."; - public String SetStats_set_pdb = "Pyro DMG Bonus set to %s%."; - public String SetStats_set_pdb_error = "Invalid Pyro DMG Bonus value."; - public String SetStats_set_cdb = "Cyro DMG Bonus set to %s%."; - public String SetStats_set_cdb_error = "Invalid Cyro DMG Bonus value."; - public String SetStats_set_hdb = "Hydro DMG Bonus set to %s%."; - public String SetStats_set_hdb_error = "Invalid Hydro DMG Bonus value."; - public String SetStats_set_adb = "Anemo DMG Bonus set to %s%."; - public String SetStats_set_adb_error = "Invalid Anemo DMG Bonus value."; - public String SetStats_set_gdb = "Geo DMG Bonus set to %s%."; - public String SetStats_set_gdb_error = "Invalid Geo DMG Bonus value."; - public String SetStats_set_edb = "Electro DMG Bonus set to %s%."; - public String SetStats_set_edb_error = "Invalid Electro DMG Bonus value."; - public String SetStats_set_physdb = "Physical DMG Bonus set to %s%."; - public String SetStats_set_physdb_error = "Invalid Physical DMG Bonus value."; - public String SetStats_set_ddb = "Dendro DMG Bonus set to %s%."; - public String SetStats_set_ddb_error = "Invalid Dendro DMG Bonus value."; - - // SetWorldLevel - public String SetWorldLevel_usage = "Usage: setworldlevel <level>"; - public String SetWorldLevel_world_level_must_between_0_and_8 = "World level must be between 0-8"; - public String SetWorldLevel_set_world_level = "World level set to %s."; - public String SetWorldLevel_invalid_world_level = "Invalid world level."; - - // Spawn - public String Spawn_usage = "Usage: spawn <entityId> [amount] [level(monster only)]"; - public String Spawn_message = "Spawned %s of %s."; - - // Stop - public String Stop_message = "Server shutting down..."; - - // Talent - public String Talent_usage_1 = "To set talent level: /talent set <talentID> <value>"; - public String Talent_usage_2 = "Another way to set talent level: /talent <n or e or q> <value>"; - public String Talent_usage_3 = "To get talent ID: /talent getid"; - public String Talent_lower_16 = "Invalid talent level. Level should be lower than 16"; - public String Talent_set_atk = "Set talent Normal ATK to %s."; - public String Talent_set_e = "Set talent E to %s."; - public String Talent_set_q = "Set talent Q to %s."; - public String Talent_invalid_skill_id = "Invalid skill ID."; - public String Talent_set_this = "Set this talent to %s."; - public String Talent_invalid_talent_level = "Invalid talent level."; - public String Talent_normal_attack_id = "Normal Attack ID %s."; - public String Talent_e_skill_id = "E skill ID %s."; - public String Talent_q_skill_id = "Q skill ID %s."; - - // TeleportAll - public String TeleportAll_message = "You only can use this command in MP mode."; - - // Teleport - public String Teleport_usage_server = "Usage: /tp @<player id> <x> <y> <z> [scene id]"; - public String Teleport_usage = "Usage: /tp [@<player id>] <x> <y> <z> [scene id]"; - public String Teleport_specify_player_id = "You must specify a player id."; - public String Teleport_invalid_position = "Invalid position."; - public String Teleport_message = "Teleported %s to %s,%s,%s in scene %s"; - - // Weather - public String Weather_usage = "Usage: weather <weatherId> [climateId]"; - public String Weather_message = "Changed weather to %s with climate %s"; - public String Weather_invalid_id = "Invalid ID."; -} From 243db37fd6b71033d80551b0af63f1ff6d0a4565 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon <benjamin7006@gmail.com> Date: Thu, 5 May 2022 19:38:05 +0800 Subject: [PATCH 107/434] No misleading 404 error --- .../grasscutter/server/dispatch/http/GachaRecordHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java index 8ca47976e..8676574bb 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java @@ -47,7 +47,7 @@ public final class GachaRecordHandler implements HttpContextHandler { res.send(response); } else { - res.send("404"); + res.send("No account found."); } } } From 9ee9d7e686c64592a150b849ec4332ce0f4d49e2 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Thu, 5 May 2022 22:00:11 +0800 Subject: [PATCH 108/434] Support of Enter Room Scene --- proto/PersonalSceneJumpReq.proto | 16 ++++++++ proto/PersonalSceneJumpRsp.proto | 19 ++++++++++ .../grasscutter/data/common/PointData.java | 11 +++++- .../recv/HandlerPersonalSceneJumpReq.java | 38 +++++++++++++++++++ .../send/PacketPersonalSceneJumpRsp.java | 20 ++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 proto/PersonalSceneJumpReq.proto create mode 100644 proto/PersonalSceneJumpRsp.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerPersonalSceneJumpReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketPersonalSceneJumpRsp.java diff --git a/proto/PersonalSceneJumpReq.proto b/proto/PersonalSceneJumpReq.proto new file mode 100644 index 000000000..cc269f258 --- /dev/null +++ b/proto/PersonalSceneJumpReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message PersonalSceneJumpReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 260; + } + + uint32 point_id = 1; +} diff --git a/proto/PersonalSceneJumpRsp.proto b/proto/PersonalSceneJumpRsp.proto new file mode 100644 index 000000000..a91f6b5ca --- /dev/null +++ b/proto/PersonalSceneJumpRsp.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "Vector.proto"; + +message PersonalSceneJumpRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 228; + } + + int32 retcode = 1; + uint32 dest_scene_id = 2; + Vector dest_pos = 3; +} diff --git a/src/main/java/emu/grasscutter/data/common/PointData.java b/src/main/java/emu/grasscutter/data/common/PointData.java index fa3891d7c..492f1fc60 100644 --- a/src/main/java/emu/grasscutter/data/common/PointData.java +++ b/src/main/java/emu/grasscutter/data/common/PointData.java @@ -13,7 +13,8 @@ public class PointData { private Position tranPos; private int[] dungeonIds; private int[] dungeonRandomList; - + + private int tranSceneId; public int getId() { return id; } @@ -38,6 +39,14 @@ public class PointData { return dungeonRandomList; } + public int getTranSceneId() { + return tranSceneId; + } + + public void setTranSceneId(int tranSceneId) { + this.tranSceneId = tranSceneId; + } + public void updateDailyDungeon() { if (getDungeonRandomList() == null) { return; diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerPersonalSceneJumpReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPersonalSceneJumpReq.java new file mode 100644 index 000000000..98c6984ee --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPersonalSceneJumpReq.java @@ -0,0 +1,38 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.ScenePointEntry; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PersonalSceneJumpReqOuterClass.PersonalSceneJumpReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketPersonalSceneJumpRsp; +import emu.grasscutter.utils.Position; + + +@Opcodes(PacketOpcodes.PersonalSceneJumpReq) +public class HandlerPersonalSceneJumpReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + PersonalSceneJumpReq req = PersonalSceneJumpReq.parseFrom(payload); + + // get the scene point + String code = session.getPlayer().getSceneId() + "_" + req.getPointId(); + ScenePointEntry scenePointEntry = GameData.getScenePointEntries().get(code); + + if (scenePointEntry != null) { + float x = scenePointEntry.getPointData().getTranPos().getX(); + float y = scenePointEntry.getPointData().getTranPos().getY(); + float z = scenePointEntry.getPointData().getTranPos().getZ(); + Position pos = new Position(x, y, z); + int sceneId = scenePointEntry.getPointData().getTranSceneId(); + + session.getPlayer().getWorld().transferPlayerToScene(session.getPlayer(), sceneId, pos); + session.send(new PacketPersonalSceneJumpRsp(sceneId, pos)); + } + + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPersonalSceneJumpRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPersonalSceneJumpRsp.java new file mode 100644 index 000000000..59065b5f8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPersonalSceneJumpRsp.java @@ -0,0 +1,20 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PersonalSceneJumpRspOuterClass.PersonalSceneJumpRsp; +import emu.grasscutter.utils.Position; + +public class PacketPersonalSceneJumpRsp extends BasePacket { + + public PacketPersonalSceneJumpRsp(int sceneId, Position pos) { + super(PacketOpcodes.PersonalSceneJumpRsp); + + PersonalSceneJumpRsp proto = PersonalSceneJumpRsp.newBuilder() + .setDestSceneId(sceneId) + .setDestPos(pos.toProto()) + .build(); + + this.setData(proto); + } +} From e01c270a525498d8348af06bae6fd26ae93f5dd9 Mon Sep 17 00:00:00 2001 From: Scirese <62688390+Scirese@users.noreply.github.com> Date: Thu, 5 May 2022 21:53:38 +0800 Subject: [PATCH 109/434] Update CNLanguage to match with the latest EN version --- .../emu/grasscutter/languages/CNLanguage.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main/java/emu/grasscutter/languages/CNLanguage.java b/src/main/java/emu/grasscutter/languages/CNLanguage.java index 48123c778..ead3b4147 100644 --- a/src/main/java/emu/grasscutter/languages/CNLanguage.java +++ b/src/main/java/emu/grasscutter/languages/CNLanguage.java @@ -45,16 +45,25 @@ public final class CNLanguage { public String Invalid_playerId = "无效的玩家ID."; public String Player_not_found = "未找到此玩家."; public String Player_is_offline = "此玩家已离线."; + public String Invalid_amount = "无效的数量."; + public String Invalid_arguments = "无效的命令参数."; + public String Invalid_artifact_id = "无效的圣遗物ID."; + public String Invalid_avatar_id = "无效的角色ID."; + public String Invalid_avatar_level = "无效的角色等级."; + public String Invalid_entity_id = "无效的物品ID."; public String Invalid_item_id = "无效的物品ID."; - public String Invalid_item_or_player_id = "无效的玩家或物品ID."; + public String Invalid_item_level = "无效的物品等级."; + public String Invalid_item_refinement = "无效的精炼等级."; + public String Invalid_UID = "无效的UID."; public String Enabled = "启用"; public String Disabled = "禁用"; public String No_command_found = "未找到命令."; public String Help = "帮助"; public String Player_not_found_or_offline = "此玩家不存在或已离线."; - public String Invalid_arguments = "无效的参数."; public String Success = "成功"; - public String Invalid_entity_id = "无效的实体ID."; + public String Target_cleared = "已清除选择目标"; + public String Target_set = "接下来的命令将默认以 @{uid} 为目标。输入命令时不必继续携带UID参数。"; + public String Target_needed = "此命令需要指定一个目标用户. 输入命令时携带 <@UID> 参数或使用 /target @UID 来指定一个默认目标用户."; // Help public String Help_usage = " 用法: "; @@ -63,7 +72,6 @@ public final class CNLanguage { // Account public String Modify_user_account = "修改用户帐户"; - public String Invalid_UID = "无效的UID."; public String Account_exists = "账户已存在."; public String Account_create_UID = "UID为 {uid} 的账户已创建."; public String Account_delete = "已删除账户."; @@ -80,7 +88,7 @@ public final class CNLanguage { public String Change_screen = "切换到场景 "; public String Change_screen_not_exist = "此场景不存在。"; - // Clear + // Cleart_or_playerId public String Clear_weapons = "已清除 {name} 的武器."; public String Clear_artifacts = "已清除 {name} 的圣遗物 ."; public String Clear_materials = "已清除 {name} 的材料."; @@ -90,8 +98,9 @@ public final class CNLanguage { public String Clear_everything = "已清除 {name} 的所有物品."; // Coop - public String Coop_usage = "用法: coop <玩家ID> <房主的玩家ID>"; - + public String Coop_usage = "用法: coop <房主的UID>"; + public String Coop_success = "已将{target}拉进{host}的世界."; + // Drop public String Drop_usage = "用法: drop <物品ID|物品名> [数量]"; public String Drop_dropped_of = "已在地上丢弃 {amount} 个 {item}."; @@ -103,22 +112,17 @@ public final class CNLanguage { public String EnterDungeon_you_in_that_dungeon = "你已经在此副本中了。"; // GiveAll - public String GiveAll_usage = "用法: giveall [玩家] [数量]"; + public String GiveAll_usage = "用法: giveall [数量]"; public String GiveAll_item = "正在给予所有物品..."; public String GiveAll_done = "完成。"; - public String GiveAll_invalid_amount_or_playerId = "无效的数量或玩家ID"; // GiveArtifact public String GiveArtifact_usage = "用法: giveart|gart [玩家] <圣遗物Id> <主词条Id> [<副词条Id>[,<被强化次数>]]... [等级]"; - public String GiveArtifact_invalid_artifact_id = "无效的圣遗物Id."; public String GiveArtifact_given = "已将 {itemId} 给予 {target}."; // GiveChar public String GiveChar_usage = "用法: givechar <p玩家> <角色Id|角色名> [等级]"; public String GiveChar_given = "将等级为 {level} 的 {avatarId} 给予 {target}."; - public String GiveChar_invalid_avatar_id = "无效的角色ID"; - public String GiveChar_invalid_avatar_level = "无效的角色等级."; - public String GiveChar_invalid_avatar_or_player_id = "无效的角色ID或玩家ID."; // Give public String Give_usage = "用法: give [玩家名] <物品ID|物品名> [数量] [等级] "; @@ -129,7 +133,8 @@ public final class CNLanguage { public String Give_given_level = "已将 {amount} 个等级为 {lvl} 的 {item} 给与 {target}."; // GodMode - public String Godmode_status = "设置 {name} 的无敌模式为 {status} "; + public String Godmode_usage = "用法: godmode [on|off|toggle]"; + public String Godmode_status = "设置 {name} 的无敌模式为: {status} "; // Heal public String Heal_message = "所有角色已被治疗。"; @@ -151,7 +156,7 @@ public final class CNLanguage { public String List_message = "现有 {size} 名玩家在线:"; // Permission - public String Permission_usage = "用法: permission <add|remove> <用户名> <权限名>"; + public String Permission_usage = "用法: permission <add|remove> <权限名>"; public String Permission_add = "权限已添加。"; public String Permission_have_permission = "此玩家已拥有此权限!"; public String Permission_remove = "权限已移除。"; @@ -263,6 +268,7 @@ public final class CNLanguage { public String Talent_set_q = "设置元素爆发(q技能)等级为 {level}."; public String Talent_invalid_skill_id = "无效的技能ID。"; public String Talent_set_this = "技能等级已设为 {level}."; + public String Talent_set_id = "将技能 {id} 的等级设为 {level}."; public String Talent_invalid_talent_level = "无效的技能等级。"; public String Talent_normal_attack_id = "普通攻击技能ID {id}."; public String Talent_e_skill_id = "元素战技(e技能)ID {id}."; @@ -272,9 +278,7 @@ public final class CNLanguage { public String TeleportAll_message = "此命令仅在多人游戏下可用。"; // Teleport - public String Teleport_usage_server = "用法: /tp @<玩家ID> <x> <y> <z> [场景ID]"; - public String Teleport_usage = "用法: /tp @<玩家ID,不指定则为你自己> <x> <y> <z> [场景ID]"; - public String Teleport_specify_player_id = "你必须指定一个玩家。"; + public String Teleport_usage_server = "用法: /tp <x> <y> <z> [场景ID]"; public String Teleport_invalid_position = "无效的位置。"; public String Teleport_message = "已将 {name} 传送到场景 {id} ,坐标 {x},{y},{z}"; From 57b5e6f3d988452121a8b30588932ff80e348621 Mon Sep 17 00:00:00 2001 From: Ljzd-PRO <63289359+Ljzd-PRO@users.noreply.github.com> Date: Fri, 6 May 2022 04:32:09 +0800 Subject: [PATCH 110/434] Change the usage of command `permission` in README.md (#557) * Change the command `permission` usage * Change the command `permission` usage --- README.md | 2 +- README_zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8119b3f30..bbef2f834 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ There is a dummy user named "Server" in every player's friends list that you can | kick | kick \<player> | server.kick | Both side | Kicks the specified player from the server. (WIP) | k | | killall | killall [playerUid] [sceneId] | server.killall | Both side | Kills all entities in the current scene or specified scene of the corresponding player. | | | list | list | | Both side | Lists online players. | | -| permission | permission <add\|remove> \<username> \<permission> | * | Both side | Grants or removes a permission for a user. | | +| permission | permission <add\|remove> \<UID> \<permission> | * | Both side | Grants or removes a permission for a user. | | | position | position | | Client only | Sends your current coordinates. | pos | | reload | reload | server.reload | Both side | Reloads the server config | | | resetconst | resetconst [all] | player.resetconstellation | Client only | Resets the constellation level on your currently selected character, will need to relog after using the command to see any changes. | resetconstellation | diff --git a/README_zh-CN.md b/README_zh-CN.md index 4b4d57131..66878b4f4 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -126,7 +126,7 @@ chmod +x gradlew | kick | kick \<uid> | server.kick | 均可使用 | 从服务器中踢出指定玩家 (WIP) | k | | killall | killall [uid] [场景ID] | server.killall | 均可使用 | 杀死指定玩家世界中所在或指定场景的全部生物 | | | list | list | | 均可使用 | 列出在线玩家 | | -| permission | permission <add\|remove> <用户名> <权限节点> | * | 均可使用 | 添加或移除玩家的权限 | | +| permission | permission <add\|remove> <UID> <权限节点> | * | 均可使用 | 添加或移除玩家的权限 | | | position | position | | 仅客户端 | 获取当前坐标 | pos | | reload | reload | server.reload | 均可使用 | 重载服务器配置 | | | resetconst | resetconst [all] | player.resetconstellation | 仅客户端 | 重置当前角色的命座,重新登录即可生效 | resetconstellation | From 18b1c50d0a8096150b8752355ac4d5d953cf7b0b Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Thu, 5 May 2022 09:27:42 -0700 Subject: [PATCH 111/434] Fixes #529: fixed stamina abnormal. added fall to death. Stamina is still WIP. - Currently stamina consumption is not affected by the use of foods, talents, or the environment. - Charged attacks do no require stamina yet. - Will be fixed tomorrow. --- .../managers/MotionManager/MotionManager.java | 227 ++++++++++++++++++ .../emu/grasscutter/game/player/Player.java | 34 +++ .../recv/HandlerCombatInvocationsNotify.java | 73 +----- .../recv/HandlerEvtDoSkillSuccNotify.java | 26 ++ 4 files changed, 291 insertions(+), 69 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java diff --git a/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java b/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java new file mode 100644 index 000000000..d8d6f25b0 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java @@ -0,0 +1,227 @@ +package emu.grasscutter.game.managers.MotionManager; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.LifeState; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.VectorOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; +import emu.grasscutter.server.packet.send.PacketPlayerPropNotify; +import emu.grasscutter.utils.Position; + +import java.util.ArrayList; +import java.lang.Math; + +public class MotionManager { + + private enum Consumption { + None(0), + + // consumers + CLIMB_START(-500), + CLIMBING(-150), + CLIMB_JUMP(-2500), + DASH(-1800), + SPRINT(-360), + FLY(-60), + SWIM_DASH_START(-200), + SWIM_DASH(-200), + SWIMMING(-80), + + // restorers + STANDBY(500), + RUN(500), + WALK(500), + STANDBY_MOVE(500); + + public final int amount; + Consumption(int amount) { + this.amount = amount; + } + } + + private EntityMoveInfoOuterClass.EntityMoveInfo moveInfo; + + private MotionState previousState = MotionState.MOTION_STANDBY; + private ArrayList<Position> previousCoordinates = new ArrayList<>(); + private final Player player; + + private float landSpeed = 0; + + public MotionManager(Player player) { + previousCoordinates.add(new Position(0,0,0)); + this.player = player; + } + + public void handle(GameSession session, GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { + MotionState state = moveInfo.getMotionInfo().getState(); + setMoveInfo(moveInfo); + if (state == MotionState.MOTION_LAND_SPEED) { + setLandSpeed(moveInfo.getMotionInfo().getSpeed().getY()); + } + if (state == MotionState.MOTION_FALL_ON_GROUND) { + handleFallOnGround(session, entity); + } + } + + public void tick() { + if(Grasscutter.getConfig().OpenStamina){ + EntityMoveInfoOuterClass.EntityMoveInfo mInfo = moveInfo; + if (mInfo == null) { + return; + } + + MotionState state = moveInfo.getMotionInfo().getState(); + Consumption consumption = Consumption.None; + + boolean isMoving = false; + VectorOuterClass.Vector posVector = moveInfo.getMotionInfo().getPos(); + Position currentCoordinates = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); + + float diffX = currentCoordinates.getX() - previousCoordinates.get(0).getX(); + float diffY = currentCoordinates.getY() - previousCoordinates.get(0).getY(); + float diffZ = currentCoordinates.getZ() - previousCoordinates.get(0).getZ(); + + if (Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.3 || Math.abs(diffZ) > 0.3) { + isMoving = true; + } + + if (isMoving) { + // TODO: refactor these conditions. + // CLIMB + if (state == MotionState.MOTION_CLIMB) { + if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = Consumption.CLIMB_START; + } else { + consumption = Consumption.CLIMBING; + } + } + // JUMP + if (state == MotionState.MOTION_CLIMB_JUMP) { + if (previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = Consumption.CLIMB_JUMP; + } + } + if (state == MotionState.MOTION_JUMP) { + if (previousState == MotionState.MOTION_CLIMB) { + consumption = Consumption.CLIMB_JUMP; + } + } + // SWIM + if (state == MotionState.MOTION_SWIM_MOVE) { + consumption = Consumption.SWIMMING; + } + if (state == MotionState.MOTION_SWIM_DASH) { + if (previousState != MotionState.MOTION_SWIM_DASH) { + consumption = Consumption.SWIM_DASH_START; + } else { + consumption = Consumption.SWIM_DASH; + } + } + // DASH + if (state == MotionState.MOTION_DASH) { + if (previousState == MotionState.MOTION_DASH) { + consumption = Consumption.SPRINT; + } else { + consumption = Consumption.DASH; + } + } + // RUN and WALK + if (state == MotionState.MOTION_RUN) { + consumption = Consumption.RUN; + } + if (state == MotionState.MOTION_WALK) { + consumption = Consumption.WALK; + } + // FLY + if (state == MotionState.MOTION_FLY) { + consumption = Consumption.FLY; + } + } + // STAND + if (state == MotionState.MOTION_STANDBY) { + consumption = Consumption.STANDBY; + } + if (state == MotionState.MOTION_STANDBY_MOVE) { + consumption = Consumption.STANDBY_MOVE; + } + + GameSession session = player.getSession(); + updateStamina(session, consumption.amount); + session.send(new PacketPlayerPropNotify(session.getPlayer(), PlayerProperty.PROP_CUR_PERSIST_STAMINA)); + + Grasscutter.getLogger().debug(session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + " " + state + " " + isMoving + " " + consumption + " " + consumption.amount); + + previousState = state; + previousCoordinates.add(currentCoordinates); + if (previousCoordinates.size() > 3) { + previousCoordinates.remove(0); + } + } + } + + public void updateStamina(GameSession session, int amount) { + if (amount == 0) { + return; + } + int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); + int newStamina = currentStamina + amount; + if (newStamina < 0) { + newStamina = 0; + } + if (newStamina > playerMaxStamina) { + newStamina = playerMaxStamina; + } + session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + } + + public void setMoveInfo(EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { + this.moveInfo = moveInfo; + } + + public EntityMoveInfoOuterClass.EntityMoveInfo getMoveInfo() { + return moveInfo; + } + + public void handleFallOnGround(GameSession session, GameEntity entity) { + float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float damage = 0; + Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); + if (landSpeed < -23.5) { + damage = (float)(maxHP * 0.33); + } + if (landSpeed < -25) { + damage = (float)(maxHP * 0.5); + } + if (landSpeed < -26.5) { + damage = (float)(maxHP * 0.66); + } + if (landSpeed < -28) { + damage = (maxHP * 1); + } + float newHP = currentHP - damage; + if (newHP < 0) { + newHP = 0; + } + Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + if (newHP == 0) { + entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); + session.getPlayer().getScene().removeEntity(entity); + entity.onDeath(0); + } + } + + public void setLandSpeed(float landSpeed) { + this.landSpeed = landSpeed; + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index c76f46bf0..b1abda002 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -21,6 +21,7 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.MailHandler; +import emu.grasscutter.game.managers.MotionManager.MotionManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; @@ -121,6 +122,8 @@ public class Player { @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; private MapMarksManager mapMarksManager; + @Transient private MotionManager motionManager; + @Deprecated @SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only! @@ -161,6 +164,7 @@ public class Player { this.shopLimit = new ArrayList<>(); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); + this.motionManager = new MotionManager(this); } // On player creation @@ -187,6 +191,7 @@ public class Player { this.getRotation().set(0, 307, 0); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); + this.motionManager = new MotionManager(this); } public int getUid() { @@ -969,6 +974,8 @@ public class Player { return mapMarksManager; } + public MotionManager getMotionManager() { return motionManager; } + public synchronized void onTick() { // Check ping if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { @@ -997,8 +1004,35 @@ public class Player { this.resetSendPlayerLocTime(); } } + + scheduleStaminaNotify(); } + private void scheduleStaminaNotify() { + // stamina tick + EntityMoveInfoOuterClass.EntityMoveInfo moveInfo = getMotionManager().getMoveInfo(); + if (moveInfo == null) { + return; + } + + if (getMotionManager().getMoveInfo().getMotionInfo().getState() == MotionStateOuterClass.MotionState.MOTION_STANDBY) { + if (getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) == getProperty(PlayerProperty.PROP_MAX_STAMINA) ) { + return; + } + } + + for (int i = 0; i <= 1000; i+=200) { + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + getMotionManager().tick(); + } + }, i); + } + } + + public void resetSendPlayerLocTime() { this.nextSendPlayerLocTime = System.currentTimeMillis() + 5000; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 2d0ceac8b..878997e22 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -1,8 +1,9 @@ package emu.grasscutter.server.packet.recv; -import emu.grasscutter.Grasscutter; import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.managers.MotionManager.MotionManager; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.LifeState; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; @@ -11,13 +12,9 @@ import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.*; -import java.util.Arrays; -import java.util.Collection; - @Opcodes(PacketOpcodes.CombatInvocationsNotify) public class HandlerCombatInvocationsNotify extends PacketHandler { @@ -35,7 +32,6 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { // Handle movement EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); - MotionState state = moveInfo.getMotionInfo().getState(); if (entity != null) { //move entity.getPosition().set(moveInfo.getMotionInfo().getPos()); @@ -43,56 +39,7 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); entity.setMotionState(moveInfo.getMotionInfo().getState()); - - if(Grasscutter.getConfig().OpenStamina){ - //consume stamina - int curStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); - int maxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); - if (CONSUME_STAMINA_LIST.contains(state)) { - - //In the water exhausted stamina - - //Climbing the wall stays in place - - //Sprint in the water - if (state == MotionState.MOTION_SWIM_DASH) { - curStamina -= 700; - } - //wall jump - else if (state == MotionState.MOTION_CLIMB_JUMP) { - curStamina -= 2000; - } - //climb the wall slowly - else if (state == MotionState.MOTION_CLIMB) { - curStamina -= 800; - } - else if (state == MotionState.MOTION_DASH_BEFORE_SHAKE) { - curStamina -= 2500; - } - else { - curStamina -= 500; - } - - session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, curStamina); - session.send(new PacketPlayerPropNotify(session.getPlayer(), PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - break; - } - //restore stamina - if (RESTORE_STAMINA_LIST.contains(state)) { - if(state == MotionState.MOTION_STANDBY) { - Vector speed = moveInfo.getMotionInfo().getSpeed(); - if(speed.getX() != 0 && speed.getZ() != 0 && speed.getY() != 0) { - break; - } - } - curStamina += 1000; - if (curStamina >= maxStamina) { - curStamina = maxStamina; - } - session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, curStamina); - session.send(new PacketPlayerPropNotify(session.getPlayer(), PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - } - } + session.getPlayer().getMotionManager().handle(session, entity, moveInfo); } break; default: @@ -111,17 +58,5 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { } } - private static MotionState[] consumeStaminaTypes = new MotionState[]{ - MotionState.MOTION_CLIMB, MotionState.MOTION_CLIMB_JUMP, MotionState.MOTION_SWIM_DASH, - MotionState.MOTION_SWIM_MOVE, MotionState.MOTION_FLY, MotionState.MOTION_DASH, - MotionState.MOTION_DASH_BEFORE_SHAKE, MotionState.MOTION_FIGHT, MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY, - MotionState.MOTION_FLY_SLOW - }; - private static MotionState[] restoreStaminaTypes = new MotionState[]{ - MotionState.MOTION_STANDBY, MotionState.MOTION_RUN, MotionState.MOTION_WALK, - MotionState.MOTION_STANDBY_MOVE - }; - private static final Collection<MotionState> CONSUME_STAMINA_LIST = Arrays.asList(consumeStaminaTypes); - private static final Collection<MotionState> RESTORE_STAMINA_LIST = Arrays.asList(restoreStaminaTypes); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java new file mode 100644 index 000000000..d1db944d2 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.EvtDoSkillSuccNotify) +public class HandlerEvtDoSkillSuccNotify extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); + // TODO: Will be used for deducting stamina for charged skills. + + int caster = notify.getCasterId(); + int skill = notify.getSkillId(); + + // Grasscutter.getLogger().warn(caster + "\t" + skill); + +// session.getPlayer().getScene().broadcastPacket(new PacketEvtAvatarStandUpNotify(notify)); + } + +} From eaf0dd0b3b4f02dd943933428f04c56b926e6ee4 Mon Sep 17 00:00:00 2001 From: ShiroSaki <62388797+ShigemoriHakura@users.noreply.github.com> Date: Fri, 6 May 2022 07:43:45 +0800 Subject: [PATCH 112/434] Add all AvatarExpedition protos Expedition system has almost done but still has some bug so it will be uploaded later --- proto/AvatarExpeditionAllDataRsp.proto | 19 +++++++++++++++++++ proto/AvatarExpeditionCallBackReq.proto | 16 ++++++++++++++++ proto/AvatarExpeditionCallBackRsp.proto | 17 +++++++++++++++++ proto/AvatarExpeditionDataNotify.proto | 16 ++++++++++++++++ proto/AvatarExpeditionGetRewardReq.proto | 16 ++++++++++++++++ proto/AvatarExpeditionGetRewardRsp.proto | 19 +++++++++++++++++++ proto/AvatarExpeditionInfo.proto | 12 ++++++++++++ proto/AvatarExpeditionStartReq.proto | 18 ++++++++++++++++++ proto/AvatarExpeditionStartRsp.proto | 17 +++++++++++++++++ 9 files changed, 150 insertions(+) create mode 100644 proto/AvatarExpeditionAllDataRsp.proto create mode 100644 proto/AvatarExpeditionCallBackReq.proto create mode 100644 proto/AvatarExpeditionCallBackRsp.proto create mode 100644 proto/AvatarExpeditionDataNotify.proto create mode 100644 proto/AvatarExpeditionGetRewardReq.proto create mode 100644 proto/AvatarExpeditionGetRewardRsp.proto create mode 100644 proto/AvatarExpeditionInfo.proto create mode 100644 proto/AvatarExpeditionStartReq.proto create mode 100644 proto/AvatarExpeditionStartRsp.proto diff --git a/proto/AvatarExpeditionAllDataRsp.proto b/proto/AvatarExpeditionAllDataRsp.proto new file mode 100644 index 000000000..5ff61dbcd --- /dev/null +++ b/proto/AvatarExpeditionAllDataRsp.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionInfo.proto"; + +message AvatarExpeditionAllDataRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1783; + } + + int32 retcode = 1; + map<uint64, AvatarExpeditionInfo> expedition_info_map = 2; + repeated uint32 open_expedition_list = 3; + uint32 expedition_count_limit = 4; +} diff --git a/proto/AvatarExpeditionCallBackReq.proto b/proto/AvatarExpeditionCallBackReq.proto new file mode 100644 index 000000000..b1131a2aa --- /dev/null +++ b/proto/AvatarExpeditionCallBackReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message AvatarExpeditionCallBackReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 1618; + } + + repeated uint64 avatar_guid = 1; +} diff --git a/proto/AvatarExpeditionCallBackRsp.proto b/proto/AvatarExpeditionCallBackRsp.proto new file mode 100644 index 000000000..75adca0f6 --- /dev/null +++ b/proto/AvatarExpeditionCallBackRsp.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionInfo.proto"; + +message AvatarExpeditionCallBackRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1633; + } + + int32 retcode = 1; + map<uint64, AvatarExpeditionInfo> expedition_info_map = 2; +} diff --git a/proto/AvatarExpeditionDataNotify.proto b/proto/AvatarExpeditionDataNotify.proto new file mode 100644 index 000000000..8adef648a --- /dev/null +++ b/proto/AvatarExpeditionDataNotify.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionInfo.proto"; + +message AvatarExpeditionDataNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1621; + } + + map<uint64, AvatarExpeditionInfo> expedition_info_map = 1; +} diff --git a/proto/AvatarExpeditionGetRewardReq.proto b/proto/AvatarExpeditionGetRewardReq.proto new file mode 100644 index 000000000..847f96fee --- /dev/null +++ b/proto/AvatarExpeditionGetRewardReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message AvatarExpeditionGetRewardReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 1610; + } + + uint64 avatar_guid = 1; +} diff --git a/proto/AvatarExpeditionGetRewardRsp.proto b/proto/AvatarExpeditionGetRewardRsp.proto new file mode 100644 index 000000000..c494b77a2 --- /dev/null +++ b/proto/AvatarExpeditionGetRewardRsp.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionInfo.proto"; + +import "ItemParam.proto"; +message AvatarExpeditionGetRewardRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1670; + } + + int32 retcode = 1; + map<uint64, AvatarExpeditionInfo> expedition_info_map = 2; + repeated ItemParam item_list = 3; +} diff --git a/proto/AvatarExpeditionInfo.proto b/proto/AvatarExpeditionInfo.proto new file mode 100644 index 000000000..180482bd0 --- /dev/null +++ b/proto/AvatarExpeditionInfo.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionState.proto"; + +message AvatarExpeditionInfo { + AvatarExpeditionState state = 1; + uint32 exp_id = 2; + uint32 hour_time = 3; + uint32 start_time = 4; + float shorten_ratio = 5; +} diff --git a/proto/AvatarExpeditionStartReq.proto b/proto/AvatarExpeditionStartReq.proto new file mode 100644 index 000000000..cbfe1a15d --- /dev/null +++ b/proto/AvatarExpeditionStartReq.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message AvatarExpeditionStartReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 1609; + } + + uint64 avatar_guid = 1; + uint32 exp_id = 2; + uint32 hour_time = 3; +} diff --git a/proto/AvatarExpeditionStartRsp.proto b/proto/AvatarExpeditionStartRsp.proto new file mode 100644 index 000000000..c48fe2ad4 --- /dev/null +++ b/proto/AvatarExpeditionStartRsp.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "AvatarExpeditionInfo.proto"; + +message AvatarExpeditionStartRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1646; + } + + int32 retcode = 1; + map<uint64, AvatarExpeditionInfo> expedition_info_map = 2; +} From 414fad907972e2c5a13d47cfa839b42a6d216c9c Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Thu, 5 May 2022 17:17:27 -0700 Subject: [PATCH 113/434] Kick player if they use an invalid resources folder --- .../server/packet/recv/HandlerSetPlayerBornDataReq.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 cae0cce65..02cdb1b8a 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java @@ -37,6 +37,13 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler { return; } + // Make sure resources folder is set + if (!GameData.getAvatarDataMap().containsKey(avatarId)) { + Grasscutter.getLogger().error("No avatar data found! Please check your ExcelBinOutput folder."); + session.close(); + return; + } + String nickname = req.getNickName(); if (nickname == null) { nickname = "Traveler"; From 2dc6a48403977f97018b65fbf93a1add872199e0 Mon Sep 17 00:00:00 2001 From: memetrollsXD <paris@memetrolls.net> Date: Fri, 6 May 2022 04:11:08 +0200 Subject: [PATCH 114/434] Customise sender and title too. Add statement of use --- src/main/java/emu/grasscutter/Config.java | 4 +++- src/main/java/emu/grasscutter/data/GameData.java | 2 -- .../emu/grasscutter/database/DatabaseHelper.java | 2 -- .../emu/grasscutter/net/packet/PacketOpcodes.java | 1 - .../packet/recv/HandlerSetPlayerBornDataReq.java | 12 ++++-------- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index dd24ac8a0..e7536b588 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -82,7 +82,9 @@ public final class Config { public int ServerAvatarId = 10000007; public int[] WelcomeEmotes = {2007, 1002, 4010}; public String WelcomeMotd = "Welcome to Grasscutter emu"; - public String WelcomeMailContent = "Hi there!\r\nFirst of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r\n\r\nCheck out our:\r\n<type=\"browser\" text=\"Discord\" href=\"https://discord.gg/T5vZU6UyeG\"/> <type=\"browser\" text=\"GitHub\" href=\"https://github.com/Melledy/Grasscutter\"/>"; + public String WelcomeMailTitle = "Welcome to Grasscutter!"; + public String WelcomeMailSender = "Lawnmower"; + public String WelcomeMailContent = "Hi there!\r\nFirst of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r\n\r\nCheck out our:\r\n<type=\"browser\" text=\"Discord\" href=\"https://discord.gg/T5vZU6UyeG\"/>"; public Mail.MailItem[] WelcomeMailItems = { new Mail.MailItem(13509, 1, 1), new Mail.MailItem(201, 10000, 1), diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index e97e1e7de..692427496 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -74,8 +74,6 @@ public class GameData { private static Map<Integer, List<ShopGoodsData>> shopGoods = new HashMap<>(); private static final IntList scenePointIdList = new IntArrayList(); - public static char EJWOA = 's'; - public static Int2ObjectMap<?> getMapByResourceDef(Class<?> resourceDefinition) { Int2ObjectMap<?> map = null; diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 97bd6d739..f63798988 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -234,6 +234,4 @@ public final class DatabaseHelper { DeleteResult result = DatabaseManager.getDatastore().delete(mail); return result.wasAcknowledged(); } - - public static char AWJVN = 'e'; } diff --git a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java index 0a0c577d0..4d9eb57e8 100644 --- a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java +++ b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java @@ -3,7 +3,6 @@ package emu.grasscutter.net.packet; public class PacketOpcodes { // Empty public static final int NONE = 0; - public static final char ONLWE = 'u'; // Opcodes public static final int AbilityChangeNotify = 1179; 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 02cdb1b8a..2487df063 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java @@ -85,15 +85,11 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler { session.send(new BasePacket(PacketOpcodes.SetPlayerBornDataRsp)); // Default mail - char d = 'G'; - char e = 'r'; - char z = 'a'; - char u = 'c'; - char s = 't'; MailBuilder mailBuilder = new MailBuilder(player.getUid(), new Mail()); - mailBuilder.mail.mailContent.title = String.format("W%sl%som%s to %s%s%s%s%s%s%s%s%s%s%s!", DatabaseHelper.AWJVN, u, DatabaseHelper.AWJVN, d, e, z, GameData.EJWOA, GameData.EJWOA, u, PacketOpcodes.ONLWE, s, s, DatabaseHelper.AWJVN, e); - mailBuilder.mail.mailContent.sender = String.format("L%swnmow%s%s @ Gi%sH%sb", z, DatabaseHelper.AWJVN, e, s, PacketOpcodes.ONLWE); - mailBuilder.mail.mailContent.content = Grasscutter.getConfig().GameServer.WelcomeMailContent; + mailBuilder.mail.mailContent.title = Grasscutter.getConfig().GameServer.WelcomeMailTitle; + mailBuilder.mail.mailContent.sender = Grasscutter.getConfig().GameServer.WelcomeMailSender; + // Please credit Grasscutter if changing something here. We don't condone commercial use of the project. + mailBuilder.mail.mailContent.content = Grasscutter.getConfig().GameServer.WelcomeMailContent + "\n<type=\"browser\" text=\"GitHub\" href=\"https://github.com/Melledy/Grasscutter\"/>"; mailBuilder.mail.itemList.addAll(Arrays.asList(Grasscutter.getConfig().GameServer.WelcomeMailItems)); mailBuilder.mail.importance = 1; player.sendMail(mailBuilder.mail); From 023c5baffeb63fcd324041898a2d9da8fb9ca9d7 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Thu, 5 May 2022 23:57:55 -0400 Subject: [PATCH 115/434] Convert to the superior language system. (pt. 1) --- build.gradle | 2 +- .../java/emu/grasscutter/Grasscutter.java | 79 ++--- .../emu/grasscutter/languages/CNLanguage.java | 285 ----------------- .../emu/grasscutter/languages/Language.java | 300 ------------------ .../java/emu/grasscutter/plugin/api/README.md | 2 - .../java/emu/grasscutter/utils/Language.java | 82 +++++ 6 files changed, 104 insertions(+), 646 deletions(-) delete mode 100644 src/main/java/emu/grasscutter/languages/CNLanguage.java delete mode 100644 src/main/java/emu/grasscutter/languages/Language.java delete mode 100644 src/main/java/emu/grasscutter/plugin/api/README.md create mode 100644 src/main/java/emu/grasscutter/utils/Language.java diff --git a/build.gradle b/build.gradle index 6da62b032..8c9257777 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ plugins { // Eclipse Support id 'eclipse' - // Intelij Support + // IntelliJ Support id 'idea' // Maven diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index f322e5fdf..bc893ef33 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -4,9 +4,7 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOError; -import java.net.InetSocketAddress; import java.util.Calendar; -import java.util.Locale; import emu.grasscutter.command.CommandMap; import emu.grasscutter.plugin.PluginManager; @@ -28,24 +26,25 @@ import com.google.gson.GsonBuilder; import ch.qos.logback.classic.Logger; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.database.DatabaseManager; -import emu.grasscutter.languages.CNLanguage; -import emu.grasscutter.languages.Language; +import emu.grasscutter.utils.Language; import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; +import static emu.grasscutter.utils.Language.translate; + public final class Grasscutter { private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class); - private static Config config; private static LineReader consoleLineReader = null; + + private static Config config; private static Language language; - private static CNLanguage cn_language; private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private static final File configFile = new File("./config.json"); - private static int day; // Current day of week + private static int day; // Current day of week. private static DispatchServer dispatchServer; private static GameServer gameServer; @@ -60,7 +59,7 @@ public final class Grasscutter { // Load server configuration. Grasscutter.loadConfig(); - // Load Language + // Load translation files. Grasscutter.loadLanguage(); // Check server structure. @@ -68,21 +67,18 @@ public final class Grasscutter { } public static void main(String[] args) throws Exception { - Crypto.loadKeys(); + Crypto.loadKeys(); // Load keys from buffers. + // Parse arguments. for (String arg : args) { switch (arg.toLowerCase()) { - case "-handbook" -> { - Tools.createGmHandbook(); return; - } - case "-gachamap" -> { - Tools.createGachaMapping(); return; - } + case "-handbook" -> Tools.createGmHandbook(); + case "-gachamap" -> Tools.createGachaMapping(); } } // Initialize server. - Grasscutter.getLogger().info(language.Starting_Grasscutter); + Grasscutter.getLogger().info(translate("messages.status.starting")); // Load all resources. Grasscutter.updateDayOfWeek(); @@ -97,7 +93,7 @@ public final class Grasscutter { // Create server instances. dispatchServer = new DispatchServer(); - gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + gameServer = new GameServer(); // Create a server hook instance with both servers. new ServerHook(gameServer, dispatchServer); @@ -110,9 +106,9 @@ public final class Grasscutter { } else if (getConfig().RunMode == ServerRunMode.GAME_ONLY) { gameServer.start(); } else { - getLogger().error(language.Invalid_server_run_mode + " " + getConfig().RunMode); - getLogger().error(language.Server_run_mode); - getLogger().error(language.Shutting_down); + getLogger().error(translate("messages.status.run_mode_error", getConfig().RunMode)); + getLogger().error(translate("messages.status.run_mode_help")); + getLogger().error(translate("messages.status.shutdown")); System.exit(1); } @@ -145,41 +141,8 @@ public final class Grasscutter { } public static void loadLanguage() { - try (FileReader file = new FileReader(String.format("%s%s.json", getConfig().LANGUAGE_FOLDER, Grasscutter.config.LocaleLanguage))) { - language = gson.fromJson(file, Language.class); - } catch (Exception e) { - Grasscutter.language = new Language(); - Grasscutter.cn_language = new CNLanguage(); - Grasscutter.config.LocaleLanguage = Locale.getDefault(); - saveConfig(); - - try { - File folder = new File("./languages"); - if (!folder.exists() && !folder.isDirectory()) { - //noinspection ResultOfMethodCallIgnored - folder.mkdirs(); - } - } catch (Exception ee) { - Grasscutter.getLogger().error("Unable to create language folder."); - } - try (FileWriter file = new FileWriter("./languages/" + Locale.US + ".json")) { - file.write(gson.toJson(language)); - } catch (Exception ee) { - Grasscutter.getLogger().error("Unable to create language file."); - } - try (FileWriter file = new FileWriter("./languages/" + Locale.SIMPLIFIED_CHINESE + ".json")) { - file.write(gson.toJson(cn_language)); - } catch (Exception ee) { - Grasscutter.getLogger().error("无法创建简体中文语言文件。"); - } - - // try again - try (FileReader file = new FileReader(String.format("%s%s.json", getConfig().LANGUAGE_FOLDER, Grasscutter.config.LocaleLanguage))) { - language = gson.fromJson(file, Language.class); - } catch (Exception ee) { - Grasscutter.getLogger().error("Unable to load " + Grasscutter.config.LocaleLanguage + ".json"); - } - } + var locale = config.LocaleLanguage; + language = Language.getLanguage(locale.toLanguageTag()); } public static void saveConfig() { @@ -193,11 +156,11 @@ public final class Grasscutter { public static void startConsole() { // Console should not start in dispatch only mode. if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { - getLogger().info(language.Dispatch_mode_not_support_command); + getLogger().info(translate("messages.dispatch.no_commands_error")); return; } - getLogger().info(language.Start_done); + getLogger().info(translate("messages.status.done")); String input = null; boolean isLastInterrupted = false; while (true) { @@ -223,7 +186,7 @@ public final class Grasscutter { try { CommandMap.getInstance().invoke(null, null, input); } catch (Exception e) { - Grasscutter.getLogger().error(language.Command_error, e); + Grasscutter.getLogger().error(translate("messages.game.command_error"), e); } } } diff --git a/src/main/java/emu/grasscutter/languages/CNLanguage.java b/src/main/java/emu/grasscutter/languages/CNLanguage.java deleted file mode 100644 index 48123c778..000000000 --- a/src/main/java/emu/grasscutter/languages/CNLanguage.java +++ /dev/null @@ -1,285 +0,0 @@ -package emu.grasscutter.languages; - -public final class CNLanguage { - public String An_error_occurred_during_game_update = "游戏更新时发生了错误."; - public String Starting_Grasscutter = "正在开启Grasscutter..."; - public String Invalid_server_run_mode = "无效的服务器运行模式. "; - public String Server_run_mode = "服务器运行模式必须为以下几种之一: 'HYBRID'(混合模式), 'DISPATCH_ONLY'(仅dispatch模式), 或 'GAME_ONLY'(仅游戏模式). 无法启动Grasscutter..."; - public String Shutting_down = "正在停止...."; - public String Start_done = "加载完成!需要指令帮助请输入 \"help\""; - public String Dispatch_mode_not_support_command = "仅dispatch模式无法使用指令。"; - public String Command_error = "命令错误:"; - public String Error = "出现错误."; - public String Grasscutter_is_free = "Grasscutter是免费软件,如果你是花钱买到的,你大概被骗了。主页: https://github.com/Grasscutters/Grasscutter"; - public String Game_start_port = "游戏服务器已在端口 {port} 上开启。"; - public String Client_connect = "来自 {address} 的客户端已连接。"; - public String Client_disconnect = "来自 {address} 的客户端已断开。"; - public String Client_request = "[Dispatch] 客户端 {ip} 请求: {method} {url}"; - public String Not_load_keystore = "[Dispatch] 无法加载证书,正在尝试默认密码..."; - public String Use_default_keystore = "[Dispatch] 成功使用默认密码加载证书. 请考虑将config.json中的KeystorePassword项改为123456."; - public String Load_keystore_error = "[Dispatch] 加载证书时出现错误!"; - public String Not_find_ssl_cert = "[Dispatch] 未找到SSL证书,正在回滚至HTTP模式。"; - public String Welcome = "欢迎使用Grasscutter"; - public String Potential_unhandled_request = "[Dispatch] 潜在的未处理请求: {method} {url}"; - public String Client_login_token = "[Dispatch] 客户端 {ip} 正在尝试使用token登录..."; - public String Client_token_login_failed = "[Dispatch] 客户端 {ip} 使用token登录失败。"; - public String Client_login_in_token = "[Dispatch] 客户端 {ip} 使用token以 {uid} 的身份登录。"; - public String Game_account_cache_error = "游戏账户缓存出现错误。"; - public String Wrong_session_key = "会话密钥错误。"; - public String Client_exchange_combo_token = "[Dispatch] 客户端 {ip} 成功交换token。"; - public String Client_failed_exchange_combo_token = "[Dispatch] 客户端 {ip} 交换token失败。"; - public String Dispatch_start_server_port = "[Dispatch] Dispatch服务器已在端口 {port} 上开启。"; - public String Client_failed_login_account_create = "[Dispatch] 客户端 {ip} 登录失败: 已创建UID为 {uid} 的账户。"; - public String Client_failed_login_account_create_failed = "[Dispatch] 客户端 {ip} 登录失败: 创建账户失败。"; - public String Client_failed_login_account_no_found = "[Dispatch] 客户端 {ip} 登录失败: 未找到帐户。"; - public String Client_login = "[Dispatch] 客户端 {ip} 以 {uid} 的身份登录。"; - public String Username_not_found = "未找到此用户名."; - public String Username_not_found_create_failed = "未找到此用户名, 创建失败。."; - - // Command - public String No_command_specified = "未指定命令."; - public String Unknown_command = "未知命令: "; - public String You_not_permission_run_command = "你没有权限运行此命令."; - public String This_command_can_only_run_from_console = "此命令只能在控制台运行."; - public String Run_this_command_in_game = "请在游戏内运行此命令."; - public String Invalid_playerId = "无效的玩家ID."; - public String Player_not_found = "未找到此玩家."; - public String Player_is_offline = "此玩家已离线."; - public String Invalid_item_id = "无效的物品ID."; - public String Invalid_item_or_player_id = "无效的玩家或物品ID."; - public String Enabled = "启用"; - public String Disabled = "禁用"; - public String No_command_found = "未找到命令."; - public String Help = "帮助"; - public String Player_not_found_or_offline = "此玩家不存在或已离线."; - public String Invalid_arguments = "无效的参数."; - public String Success = "成功"; - public String Invalid_entity_id = "无效的实体ID."; - - // Help - public String Help_usage = " 用法: "; - public String Help_aliases = " 别名: "; - public String Help_available_command = " 可用命令:"; - - // Account - public String Modify_user_account = "修改用户帐户"; - public String Invalid_UID = "无效的UID."; - public String Account_exists = "账户已存在."; - public String Account_create_UID = "UID为 {uid} 的账户已创建."; - public String Account_delete = "已删除账户."; - public String Account_not_find = "账户不存在."; - public String Account_command_usage = "用法: account <create(创建)|delete(删除)> <用户名> [uid]"; - - // Broadcast - public String Broadcast_command_usage = "用法: broadcast <消息>"; - public String Broadcast_message_sent = "消息已发送."; - - // ChangeScene - public String Change_screen_usage = "用法: changescene <场景id>"; - public String Change_screen_you_in_that_screen = "你已经在此场景中了"; - public String Change_screen = "切换到场景 "; - public String Change_screen_not_exist = "此场景不存在。"; - - // Clear - public String Clear_weapons = "已清除 {name} 的武器."; - public String Clear_artifacts = "已清除 {name} 的圣遗物 ."; - public String Clear_materials = "已清除 {name} 的材料."; - public String Clear_furniture = "已清除 {name} 的摆设."; - public String Clear_displays = "已清除 {name} 的displays."; - public String Clear_virtuals = "已清除 {name} 的virtuals."; - public String Clear_everything = "已清除 {name} 的所有物品."; - - // Coop - public String Coop_usage = "用法: coop <玩家ID> <房主的玩家ID>"; - - // Drop - public String Drop_usage = "用法: drop <物品ID|物品名> [数量]"; - public String Drop_dropped_of = "已在地上丢弃 {amount} 个 {item}."; - - // EnterDungeon - public String EnterDungeon_usage = "用法: enterdungeon <副本 id>"; - public String EnterDungeon_changed_to_dungeon = "已进入副本 "; - public String EnterDungeon_dungeon_not_found = "副本不存在"; - public String EnterDungeon_you_in_that_dungeon = "你已经在此副本中了。"; - - // GiveAll - public String GiveAll_usage = "用法: giveall [玩家] [数量]"; - public String GiveAll_item = "正在给予所有物品..."; - public String GiveAll_done = "完成。"; - public String GiveAll_invalid_amount_or_playerId = "无效的数量或玩家ID"; - - // GiveArtifact - public String GiveArtifact_usage = "用法: giveart|gart [玩家] <圣遗物Id> <主词条Id> [<副词条Id>[,<被强化次数>]]... [等级]"; - public String GiveArtifact_invalid_artifact_id = "无效的圣遗物Id."; - public String GiveArtifact_given = "已将 {itemId} 给予 {target}."; - - // GiveChar - public String GiveChar_usage = "用法: givechar <p玩家> <角色Id|角色名> [等级]"; - public String GiveChar_given = "将等级为 {level} 的 {avatarId} 给予 {target}."; - public String GiveChar_invalid_avatar_id = "无效的角色ID"; - public String GiveChar_invalid_avatar_level = "无效的角色等级."; - public String GiveChar_invalid_avatar_or_player_id = "无效的角色ID或玩家ID."; - - // Give - public String Give_usage = "用法: give [玩家名] <物品ID|物品名> [数量] [等级] "; - public String Give_refinement_only_applicable_weapons = "精炼只对武器有效。"; - public String Give_refinement_must_between_1_and_5 = "精炼等级必须在1和5之间。"; - public String Give_given = "已将 {amount} 个 {item} 给与 {target}."; - public String Give_given_with_level_and_refinement = "已将 {amount} 个等级为 {lvl}, 精炼 {refinement} 的 {item} 给予 {target}."; - public String Give_given_level = "已将 {amount} 个等级为 {lvl} 的 {item} 给与 {target}."; - - // GodMode - public String Godmode_status = "设置 {name} 的无敌模式为 {status} "; - - // Heal - public String Heal_message = "所有角色已被治疗。"; - - // Kick - public String Kick_player_kick_player = "玩家 [{sendUid}:{sendName}] 已踢出 [{kickUid}:{kickName}]"; - public String Kick_server_player = "正在踢出玩家 [{kickUid}:{kickName}]"; - - // Kill - public String Kill_usage = "用法: killall [玩家UID] [场景ID]"; - public String Kill_scene_not_found_in_player_world = "未在玩家世界中找到此场景"; - public String Kill_kill_monsters_in_scene = "已杀死场景 {id} 中的 {size} 只怪物。 "; - - // KillCharacter - public String KillCharacter_usage = "用法: /killcharacter [玩家Id]"; - public String KillCharacter_kill_current_character = "已干掉 {name} 当前的场上角色."; - - // List - public String List_message = "现有 {size} 名玩家在线:"; - - // Permission - public String Permission_usage = "用法: permission <add|remove> <用户名> <权限名>"; - public String Permission_add = "权限已添加。"; - public String Permission_have_permission = "此玩家已拥有此权限!"; - public String Permission_remove = "权限已移除。"; - public String Permission_not_have_permission = "此玩家未拥有此权限!"; - - // Position - public String Position_message = "坐标: {x},{y},{z}\n场景: {id}"; - - // Reload - public String Reload_reload_start = "正在重新加载配置."; - public String Reload_reload_done = "完成."; - - // ResetConst - public String ResetConst_reset_all = "重置你所有角色的命座。"; - public String ResetConst_reset_all_done = "{name} 的命座已被重置。请重新登录。"; - - // ResetShopLimit - public String ResetShopLimit_usage = "用法: /resetshop <玩家id>"; - - // SendMail - public String SendMail_usage = "用法: give [player] <itemId|itemName> [amount]"; - public String SendMail_user_not_exist = "不存在id为 '{id}' 的用户。"; - public String SendMail_start_composition = "开始编辑邮件的组成部分.\n请使用 `/sendmail <标题>` 以继续.\n你可以在任何时候使用`/sendmail stop` 来停止编辑."; - public String SendMail_templates = "很快就会有邮件模板了......."; - public String SendMail_invalid_arguments = "无效的参数.\n用法: `/sendmail <用户Id|all|help> [模板Id]``"; - public String SendMail_send_cancel = "已取消发送邮件。"; - public String SendMail_send_done = "已向 {name} 发送邮件!"; - public String SendMail_send_all_done = "已向所有玩家发送邮件!"; - public String SendMail_not_composition_end = "邮件组成部分编辑尚未结束.\n请使用 `/sendmail {args}` 或 `/sendmail stop` 来停止编辑"; - public String SendMail_Please_use = "请使用 `/sendmail {args}`"; - public String SendMail_set_title = "邮件标题已设为 '{title}'.\n使用 '/sendmail <邮件正文>' 以继续."; - public String SendMail_set_contents = "邮件的正文如下:\n '{contents}'\n使用 '/sendmail <发送者署名>' 以继续."; - public String SendMail_set_message_sender = "邮件的发送者已设为 '{send}'.\n使用 '/sendmail <物品Id|物品名|finish(结束编辑并发送)> [数量] [等级]"; - public String SendMail_send = "已将 {amount} 个 {item} (等级 {lvl}) 作为邮件附件.\n你可以继续添加附件,也可以使用 `/sendmail finish` 来停止编辑并发送邮件."; - public String SendMail_invalid_arguments_please_use = "无效的参数 \n 请使用 `/sendmail {args}`"; - public String SendMail_title = "<标题>"; - public String SendMail_message = "<正文>"; - public String SendMail_sender = "<发送者>"; - public String SendMail_arguments = "<物品Id|物品名|finish(结束编辑并发送)> [数量] [等级]"; - public String SendMail_error = "错误:无效的编写阶段 {stage}. 需要stacktrace请看服务器命令行."; - - // SendMessage - public String SendMessage_usage = "用法: sendmessage <玩家名> <消息>"; - public String SenaMessage_message_sent = "已发送."; - - // SetFetterLevel - public String SetFetterLevel_usage = "用法: setfetterlevel <等级>"; - public String SetFetterLevel_fetter_level_must_between_0_and_10 = "设置的好感等级必须位于 0 和 10 之间。"; - public String SetFetterLevel_fetter_set_level = "好感等级已设置为 {level}"; - public String SetFetterLevel_invalid_fetter_level = "无效的好感等级。"; - - // SetStats - public String SetStats_usage = "用法: setstats|stats <stat> <value>"; - public String SetStats_setstats_help_message = "用法: /setstats|stats <hp(生命值) | mhp(最大生命值) | def(防御力) | atk(攻击) | em(元素精通) | crate(暴击率) | cdmg(暴击伤害)> <数值> 基本属性(整数)"; - public String SetStats_stats_help_message = "用法: /stats <er(元素充能) | epyro(火伤) | ecryo(冰伤) | ehydro(水伤) | eanemo(风伤) | egeo(岩伤) | edend(草伤) | eelec(雷伤) | ephys(物伤)> <数值> 元素属性(百分比)"; - public String SetStats_set_max_hp = "最大生命值已设为 {int}."; - public String SetStats_set_max_hp_error = "无效的生命数值."; - public String SetStats_set_hp = "生命设置为 {int}."; - public String SetStats_set_hp_error = "无效的生命数值."; - public String SetStats_set_def = "防御力设置为 {int}."; - public String SetStats_set_def_error = "无效的防御力数值."; - public String SetStats_set_atk = "攻击力设置为 {int}."; - public String SetStats_set_atk_error = "无效的攻击力数值."; - public String SetStats_set_em = "元素精通设置为 {int}."; - public String SetStats_set_em_error = "无效的元素精通数值."; - public String SetStats_set_er = "元素充能设置为 {int}%."; - public String SetStats_set_er_error = "无效的元素充能数值."; - public String SetStats_set_cr = "暴击率设置为 {int}%."; - public String SetStats_set_cr_error = "无效的暴击率数值."; - public String SetStats_set_cd = "暴击伤害设置为 {int}%."; - public String SetStats_set_cd_error = "无效的暴击伤害数值."; - public String SetStats_set_pdb = "火伤设置为 {int}%."; - public String SetStats_set_pdb_error = "无效的火伤数值."; - public String SetStats_set_cdb = "冰伤设置为 {int}%."; - public String SetStats_set_cdb_error = "无效的冰伤数值."; - public String SetStats_set_hdb = "水伤设置为 {int}%."; - public String SetStats_set_hdb_error = "无效的水伤数值."; - public String SetStats_set_adb = "风伤设置为 {int}%."; - public String SetStats_set_adb_error = "无效的风伤数值."; - public String SetStats_set_gdb = "岩伤设置为 {int}%."; - public String SetStats_set_gdb_error = "无效的岩伤数值."; - public String SetStats_set_edb = "雷伤设置为 {int}%."; - public String SetStats_set_edb_error = "无效的雷伤数值."; - public String SetStats_set_physdb = "物伤设置为 {int}%."; - public String SetStats_set_physdb_error = "无效的物伤数值."; - public String SetStats_set_ddb = "草伤设置为 {int}%."; - public String SetStats_set_ddb_error = "无效的草伤数值."; - - // SetWorldLevel - public String SetWorldLevel_usage = "用法: setworldlevel <level>"; - public String SetWorldLevel_world_level_must_between_0_and_8 = "世界等级必须在0-8之间。"; - public String SetWorldLevel_set_world_level = "世界等级已设置为 {level}."; - public String SetWorldLevel_invalid_world_level = "无效的世界等级."; - - // Spawn - public String Spawn_usage = "用法: spawn <实体ID|实体名> [数量] [等级(仅限怪物)]"; - public String Spawn_message = "已生成 {amount} 个 {id}."; - - // Stop - public String Stop_message = "正在关闭服务器..."; - - // Talent - public String Talent_usage_1 = "设置技能等级: /talent set <技能ID> <数值>"; - public String Talent_usage_2 = "另一种方式: /talent <n 或 e 或 q> <数值>"; - public String Talent_usage_3 = "获取技能ID: /talent getid"; - public String Talent_lower_16 = "技能等级应低于16。"; - public String Talent_set_atk = "设置普通攻击等级为 {level}."; - public String Talent_set_e = "设置元素战技(e技能)等级为 {level}."; - public String Talent_set_q = "设置元素爆发(q技能)等级为 {level}."; - public String Talent_invalid_skill_id = "无效的技能ID。"; - public String Talent_set_this = "技能等级已设为 {level}."; - public String Talent_invalid_talent_level = "无效的技能等级。"; - public String Talent_normal_attack_id = "普通攻击技能ID {id}."; - public String Talent_e_skill_id = "元素战技(e技能)ID {id}."; - public String Talent_q_skill_id = "元素爆发(q技能)ID {id}."; - - // TeleportAll - public String TeleportAll_message = "此命令仅在多人游戏下可用。"; - - // Teleport - public String Teleport_usage_server = "用法: /tp @<玩家ID> <x> <y> <z> [场景ID]"; - public String Teleport_usage = "用法: /tp @<玩家ID,不指定则为你自己> <x> <y> <z> [场景ID]"; - public String Teleport_specify_player_id = "你必须指定一个玩家。"; - public String Teleport_invalid_position = "无效的位置。"; - public String Teleport_message = "已将 {name} 传送到场景 {id} ,坐标 {x},{y},{z}"; - - // Weather - public String Weather_usage = "用法: weather <天气ID> [气候ID]"; - public String Weather_message = "已修改天气为 {weatherId} 气候为 {climateId}"; - public String Weather_invalid_id = "无效的ID。"; -} diff --git a/src/main/java/emu/grasscutter/languages/Language.java b/src/main/java/emu/grasscutter/languages/Language.java deleted file mode 100644 index 53e99d24f..000000000 --- a/src/main/java/emu/grasscutter/languages/Language.java +++ /dev/null @@ -1,300 +0,0 @@ -package emu.grasscutter.languages; - -public final class Language { - public String An_error_occurred_during_game_update = "An error occurred during game update."; - public String Starting_Grasscutter = "Starting Grasscutter..."; - public String Invalid_server_run_mode = "Invalid server run mode."; - public String Server_run_mode = "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter..."; - public String Shutting_down = "Shutting down..."; - public String Start_done = "Done! For help, type \"help\""; - public String Dispatch_mode_not_support_command = "Commands are not supported in dispatch only mode."; - public String Command_error = "Command error:"; - public String Error = "An error occurred."; - public String Grasscutter_is_free = "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter"; - public String Game_start_port = "Game Server started on port {port}"; - public String Client_connect = "Client connected from {address}"; - public String Client_disconnect = "Client disconnected from {address}"; - public String Client_request = "[Dispatch] Client {ip} {method} request: {url}"; - public String Not_load_keystore = "[Dispatch] Unable to load keystore. Trying default keystore password..."; - public String Use_default_keystore = "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."; - public String Load_keystore_error = "[Dispatch] Error while loading keystore!"; - public String Not_find_ssl_cert = "[Dispatch] No SSL cert found! Falling back to HTTP server."; - public String Welcome = "Welcome to Grasscutter"; - public String Potential_unhandled_request = "[Dispatch] Potential unhandled {method} request: {url}"; - public String Client_try_login = "[Dispatch] Client {ip} is trying to log in"; - public String Client_login_token = "[Dispatch] Client {ip} is trying to log in via token"; - public String Client_token_login_failed = "[Dispatch] Client {ip} failed to log in via token"; - public String Client_login_in_token = "[Dispatch] Client {ip} logged in via token as {uid}"; - public String Game_account_cache_error = "Game account cache information error"; - public String Wrong_session_key = "Wrong session key."; - public String Client_exchange_combo_token = "[Dispatch] Client {ip} succeed to exchange combo token"; - public String Client_failed_exchange_combo_token = "[Dispatch] Client {ip} failed to exchange combo token"; - public String Dispatch_start_server_port = "[Dispatch] Dispatch server started on port {port}"; - public String Client_failed_login_account_create = "[Dispatch] Client {ip} failed to log in: Account {uid} created"; - public String Client_failed_login_account_create_failed = "[Dispatch] Client {ip} failed to log in: Account create failed"; - public String Client_failed_login_account_no_found = "[Dispatch] Client {ip} failed to log in: Account no found"; - public String Client_login = "[Dispatch] Client {ip} logged in as {uid}"; - public String Username_not_found = "Username not found."; - public String Username_not_found_create_failed = "Username not found, create failed."; - public String Create_resources_folder = "Creating resources folder..."; - public String Place_copy = "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder."; - - // Command - public String No_command_specified = "No command specified."; - public String Unknown_command = "Unknown command: "; - public String You_not_permission_run_command = "You do not have permission to run this command."; - public String This_command_can_only_run_from_console = "This command can only be run from the console."; - public String Run_this_command_in_game = "Run this command in-game."; - public String Invalid_amount = "Invalid amount."; - public String Invalid_arguments = "Invalid arguments."; - public String Invalid_artifact_id = "Invalid artifact ID."; - public String Invalid_avatar_id = "Invalid avatar id."; - public String Invalid_avatar_level = "Invalid avatar level."; - public String Invalid_entity_id = "Invalid entity id."; - public String Invalid_item_id = "Invalid item id."; - public String Invalid_item_level = "Invalid item level."; - public String Invalid_item_refinement = "Invalid item refinement level."; - public String Invalid_playerId = "Invalid playerId."; - public String Invalid_UID = "Invalid UID."; - public String Player_not_found = "Player not found."; - public String Player_is_offline = "Player is offline."; - public String Enabled = "enabled"; - public String Disabled = "disabled"; - public String No_command_found = "No command found."; - public String Help = "Help"; - public String Player_not_found_or_offline = "Player not found or offline."; - public String Success = "Success"; - public String Target_cleared = "Target cleared."; - public String Target_set = "Subsequent commands will target @{uid} by default."; - public String Target_needed = "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID."; - - // Help - public String Help_usage = " Usage: "; - public String Help_aliases = " Aliases: "; - public String Help_available_command = "Available commands:"; - - // Account - public String Modify_user_account = "Modify user accounts"; - public String Account_exists = "Account already exists."; - public String Account_create_UID = "Account created with UID {uid}."; - public String Account_delete = "Account deleted."; - public String Account_not_find = "Account not found."; - public String Account_command_usage = "Usage: account <create|delete> <username> [uid]"; - - // Broadcast - public String Broadcast_command_usage = "Usage: broadcast <message>"; - public String Broadcast_message_sent = "Message sent."; - - // ChangeScene - public String Change_screen_usage = "Usage: changescene <scene id>"; - public String Change_screen_you_in_that_screen = "You are already in that scene"; - public String Change_screen = "Changed to scene "; - public String Change_screen_not_exist = "Scene does not exist"; - - // Clear - public String Clear_usage = "Usage: clear <all|wp|art|mat>"; - public String Clear_weapons = "Cleared weapons for {name} ."; - public String Clear_artifacts = "Cleared artifacts for {name} ."; - public String Clear_materials = "Cleared materials for {name} ."; - public String Clear_furniture = "Cleared furniture for {name} ."; - public String Clear_displays = "Cleared displays for {name} ."; - public String Clear_virtuals = "Cleared virtuals for {name} ."; - public String Clear_everything = "Cleared everything for {name} ."; - - // Coop - public String Coop_usage = "Usage: coop <host UID>"; - public String Coop_success = "Summoned {target} to {host}'s world."; - - // Drop - public String Drop_usage = "Usage: drop <itemId|itemName> [amount]"; - public String Drop_dropped_of = "Dropped {amount} of {item}."; - - // EnterDungeon - public String EnterDungeon_usage = "Usage: enterdungeon <dungeon id>"; - public String EnterDungeon_changed_to_dungeon = "Changed to dungeon "; - public String EnterDungeon_dungeon_not_found = "Dungeon does not exist"; - public String EnterDungeon_you_in_that_dungeon = "You are already in that dungeon"; - - // GiveAll - public String GiveAll_usage = "Usage: giveall [amount]"; - public String GiveAll_item = "Giving all items..."; - public String GiveAll_done = "Giving all items done"; - - // GiveArtifact - public String GiveArtifact_usage = "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]"; - public String GiveArtifact_given = "Given {itemId} to {target}."; - - // GiveChar - public String GiveChar_usage = "Usage: givechar <player> <itemId|itemName> [amount]"; - public String GiveChar_given = "Given {avatarId} with level {level} to {target}."; - - // Give - public String Give_usage = "Usage: give <player> <itemId|itemName> [amount] [level]"; - public String Give_refinement_only_applicable_weapons = "Refinement is only applicable to weapons."; - public String Give_refinement_must_between_1_and_5 = "Refinement must be between 1 and 5."; - public String Give_given = "Given {amount} of {item} to {target}."; - public String Give_given_with_level_and_refinement = "Given {item} with level {lvl}, refinement {refinement} {amount} times to {target}"; - public String Give_given_level = "Given {item} with level {lvl} {amount} times to {target}"; - - // GodMode - public String Godmode_usage = "Usage: godmode [on|off|toggle]"; - public String Godmode_status = "Godmode is now {status} for {name}."; - - // Heal - public String Heal_message = "All characters have been healed."; - - // Kick - public String Kick_player_kick_player = "Player [{sendUid}:{sendName}] has kicked player [{kickUid}:{kickName}]"; - public String Kick_server_player = "Kicking player [{kickUid}:{kickName}]"; - - // Kill - public String Kill_usage = "Usage: killall [playerUid] [sceneId]"; - public String Kill_scene_not_found_in_player_world = "Scene not found in player world"; - public String Kill_kill_monsters_in_scene = "Killing {size} monsters in scene {id}"; - - // KillCharacter - public String KillCharacter_usage = "Usage: /killcharacter [playerId]"; - public String KillCharacter_kill_current_character = "Killed {name} current character."; - - // List - public String List_message = "There are {size} player(s) online:"; - - // Permission - public String Permission_usage = "Usage: permission <add|remove> <permission>"; - public String Permission_add = "Permission added."; - public String Permission_have_permission = "They already have this permission!"; - public String Permission_remove = "Permission removed."; - public String Permission_not_have_permission = "They don't have this permission!"; - - // Position - public String Position_message = "Coord: {x}, {y}, {z}\nScene id: {id}"; - - // Reload - public String Reload_reload_start = "Reloading config."; - public String Reload_reload_done = "Reload complete."; - - // ResetConst - public String ResetConst_reset_all = "Reset all avatars' constellations."; - public String ResetConst_reset_all_done = "Constellations for {name} have been reset. Please relog to see changes."; - - // ResetShopLimit - public String ResetShopLimit_usage = "Usage: /resetshop <player id>"; - - // SendMail - public String SendMail_usage = "Usage: give [player] <itemId|itemName> [amount]"; - public String SendMail_user_not_exist = "The user with an id of '{id}' does not exist"; - public String SendMail_start_composition = "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time"; - public String SendMail_templates = "Mail templates coming soon implemented..."; - public String SendMail_invalid_arguments = "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`"; - public String SendMail_send_cancel = "Message sending cancelled"; - public String SendMail_send_done = "Message sent to user {name}!"; - public String SendMail_send_all_done = "Message sent to all users!"; - public String SendMail_not_composition_end = "Message composition not at final stage.\nPlease use `/sendmail {args}` or `/sendmail stop` to cancel"; - public String SendMail_please_use = "Please use `/sendmail {args}`"; - public String SendMail_set_title = "Message title set as '{title}'.\nUse '/sendmail <content>' to continue."; - public String SendMail_set_contents = "Message contents set as '{contents}'.\nUse '/sendmail <sender>' to continue."; - public String SendMail_set_message_sender = "Message sender set as '{send}'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue."; - public String SendMail_send = "Attached {amount} of {item} (level {lvl}) to the message.\nContinue adding more items or use `/sendmail finish` to send the message."; - public String SendMail_invalid_arguments_please_use = "Invalid arguments \n Please use `/sendmail {args}`"; - public String SendMail_title = "<title>"; - public String SendMail_message = "<message>"; - public String SendMail_sender = "<sender>"; - public String SendMail_arguments = "<itemId|itemName|finish> [amount] [level]"; - public String SendMail_error = "ERROR: invalid construction stage {stage}. Check console for stacktrace."; - - // SendMessage - public String SendMessage_usage = "Usage: sendmessage <player> <message>"; - public String SenaMessage_message_sent = "Message sent."; - - // SetFetterLevel - public String SetFetterLevel_usage = "Usage: setfetterlevel <level>"; - public String SetFetterLevel_fetter_level_must_between_0_and_10 = "Fetter level must be between 0 and 10."; - public String SetFetterLevel_fetter_set_level = "Fetter level set to {level}"; - public String SetFetterLevel_invalid_fetter_level = "Invalid fetter level."; - - // SetStats - public String SetStats_usage_console = "Usage: setstats|stats @<UID> <stat> <value>"; - public String SetStats_usage_ingame = "Usage: setstats|stats [@UID] <stat> <value>"; - public String SetStats_help_message = """ - \n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi - \t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys - \t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys - """; - public String SetStats_value_error = "Invalid stat value."; - public String SetStats_set_self = "{name} set to {value}."; - public String SetStats_set_for_uid = "{name} for {uid} set to {value}."; - public String Stats_FIGHT_PROP_MAX_HP = "Max HP"; - public String Stats_FIGHT_PROP_CUR_HP = "Current HP"; - public String Stats_FIGHT_PROP_CUR_ATTACK = "ATK"; - public String Stats_FIGHT_PROP_BASE_ATTACK = "Base ATK"; - public String Stats_FIGHT_PROP_DEFENSE = "DEF"; - public String Stats_FIGHT_PROP_ELEMENT_MASTERY = "Elemental Mastery"; - public String Stats_FIGHT_PROP_CHARGE_EFFICIENCY = "Energy Recharge"; - public String Stats_FIGHT_PROP_CRITICAL = "Crit Rate"; - public String Stats_FIGHT_PROP_CRITICAL_HURT = "Crit DMG"; - public String Stats_FIGHT_PROP_ADD_HURT = "DMG Bonus"; - public String Stats_FIGHT_PROP_WIND_ADD_HURT = "Anemo DMG Bonus"; - public String Stats_FIGHT_PROP_ICE_ADD_HURT = "Cryo DMG Bonus"; - public String Stats_FIGHT_PROP_GRASS_ADD_HURT = "Dendro DMG Bonus"; - public String Stats_FIGHT_PROP_ELEC_ADD_HURT = "Electro DMG Bonus"; - public String Stats_FIGHT_PROP_ROCK_ADD_HURT = "Geo DMG Bonus"; - public String Stats_FIGHT_PROP_WATER_ADD_HURT = "Hydro DMG Bonus"; - public String Stats_FIGHT_PROP_FIRE_ADD_HURT = "Pyro DMG Bonus"; - public String Stats_FIGHT_PROP_PHYSICAL_ADD_HURT = "Physical DMG Bonus"; - public String Stats_FIGHT_PROP_SUB_HURT = "DMG Reduction"; - public String Stats_FIGHT_PROP_WIND_SUB_HURT = "Anemo RES"; - public String Stats_FIGHT_PROP_ICE_SUB_HURT = "Cryo RES"; - public String Stats_FIGHT_PROP_GRASS_SUB_HURT = "Dendro RES"; - public String Stats_FIGHT_PROP_ELEC_SUB_HURT = "Electro RES"; - public String Stats_FIGHT_PROP_ROCK_SUB_HURT = "Geo RES"; - public String Stats_FIGHT_PROP_WATER_SUB_HURT = "Hydro RES"; - public String Stats_FIGHT_PROP_FIRE_SUB_HURT = "Pyro RES"; - public String Stats_FIGHT_PROP_PHYSICAL_SUB_HURT = "Physical RES"; - public String Stats_FIGHT_PROP_SKILL_CD_MINUS_RATIO = "Cooldown Reduction"; - public String Stats_FIGHT_PROP_HEAL_ADD = "Healing Bonus"; - public String Stats_FIGHT_PROP_HEALED_ADD = "Incoming Healing Bonus"; - public String Stats_FIGHT_PROP_SHIELD_COST_MINUS_RATIO = "Shield Strength"; - public String Stats_FIGHT_PROP_DEFENCE_IGNORE_RATIO = "DEF Ignore"; - - // SetWorldLevel - public String SetWorldLevel_usage = "Usage: setworldlevel <level>"; - public String SetWorldLevel_world_level_must_between_0_and_8 = "World level must be between 0-8"; - public String SetWorldLevel_set_world_level = "World level set to {level}."; - public String SetWorldLevel_invalid_world_level = "Invalid world level."; - - // Spawn - public String Spawn_usage = "Usage: spawn <entityId> [amount] [level(monster only)]"; - public String Spawn_message = "Spawned {amount} of {id}."; - - // Stop - public String Stop_message = "Server shutting down..."; - - // Talent - public String Talent_usage_1 = "To set talent level: /talent set <talentID> <value>"; - public String Talent_usage_2 = "Another way to set talent level: /talent <n or e or q> <value>"; - public String Talent_usage_3 = "To get talent ID: /talent getid"; - public String Talent_lower_16 = "Invalid talent level. Level should be lower than 16"; - public String Talent_set_id = "Set talent {id} to {level}."; - public String Talent_set_atk = "Set talent Normal ATK to {level}."; - public String Talent_set_e = "Set talent E to {level}."; - public String Talent_set_q = "Set talent Q to {level}."; - public String Talent_invalid_skill_id = "Invalid skill ID."; - public String Talent_set_this = "Set this talent to {level}."; - public String Talent_invalid_talent_level = "Invalid talent level."; - public String Talent_normal_attack_id = "Normal Attack ID {id}."; - public String Talent_e_skill_id = "E skill ID {id}."; - public String Talent_q_skill_id = "Q skill ID {id}."; - - // TeleportAll - public String TeleportAll_message = "You only can use this command in MP mode."; - - // Teleport - public String Teleport_usage = "Usage: /tp <x> <y> <z> [scene id]"; - public String Teleport_invalid_position = "Invalid position."; - public String Teleport_message = "Teleported {name} to {x},{y},{z} in scene {id}"; - - // Weather - public String Weather_usage = "Usage: weather <weatherId> [climateId]"; - public String Weather_message = "Changed weather to {weatherId} with climate {climateId}"; - public String Weather_invalid_id = "Invalid ID."; -} diff --git a/src/main/java/emu/grasscutter/plugin/api/README.md b/src/main/java/emu/grasscutter/plugin/api/README.md deleted file mode 100644 index 73a5a75ee..000000000 --- a/src/main/java/emu/grasscutter/plugin/api/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Grasscutter Plugin API -**Warning!** As of now, this is a work in progress and isn't completely documented. \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java new file mode 100644 index 000000000..702da202a --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -0,0 +1,82 @@ +package emu.grasscutter.utils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import emu.grasscutter.Grasscutter; + +import javax.annotation.Nullable; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public final class Language { + private final JsonObject languageData; + private final Map<String, String> cachedTranslations = new HashMap<>(); + + /** + * Creates a language instance from a code. + * @param langCode The language code. + * @return A language instance. + */ + public static Language getLanguage(String langCode) { + return new Language(langCode + ".json"); + } + + /** + * Returns the translated value from the key while substituting arguments. + * @param key The key of the translated value to return. + * @param args The arguments to substitute. + * @return A translated value with arguments substituted. + */ + public static String translate(String key, Object... args) { + return Grasscutter.getLanguage().get(key).formatted(args); + } + + /** + * Reads a file and creates a language instance. + * @param fileName The name of the language file. + */ + private Language(String fileName) { + @Nullable JsonObject languageData = null; + + try { + InputStream file = Grasscutter.class.getResourceAsStream("/lang/" + fileName); + languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to load language file: " + fileName, exception); + } this.languageData = languageData; + } + + /** + * Returns the value (as a string) from a nested key. + * @param key The key to look for. + * @return The value (as a string) from a nested key. + */ + public String get(String key) { + if(this.cachedTranslations.containsKey(key)) { + return this.cachedTranslations.get(key); + } + + String[] keys = key.split("\\."); + JsonObject object = this.languageData; + + int index = 0; + String result = ""; + + while (true) { + if(index == keys.length) break; + + String currentKey = keys[index++]; + if(object.has(currentKey)) { + JsonElement element = object.get(currentKey); + if(element.isJsonObject()) + object = element.getAsJsonObject(); + else { + result = element.getAsString(); break; + } + } else break; + } + + this.cachedTranslations.put(key, result); return result; + } +} From f7311968d6bcd95df8cd37370b37cdd8cfda6cb4 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 00:57:45 -0400 Subject: [PATCH 116/434] Convert to the superior language system. (pt. 2) --- .gitignore | 4 +- src/main/java/emu/grasscutter/Config.java | 1 - .../emu/grasscutter/command/CommandMap.java | 57 ++-- .../command/commands/AccountCommand.java | 19 +- .../command/commands/BroadcastCommand.java | 6 +- .../command/commands/ChangeSceneCommand.java | 16 +- .../command/commands/ClearCommand.java | 36 ++- .../command/commands/CoopCommand.java | 2 +- .../command/commands/DropCommand.java | 16 +- .../command/commands/EnterDungeonCommand.java | 16 +- .../command/commands/GiveAllCommand.java | 2 +- .../command/commands/GiveArtifactCommand.java | 2 +- .../command/commands/GiveCharCommand.java | 16 +- .../command/commands/GiveCommand.java | 28 +- .../command/commands/GodModeCommand.java | 8 +- .../command/commands/HealCommand.java | 7 +- .../command/commands/HelpCommand.java | 22 +- .../command/commands/KickCommand.java | 11 +- .../command/commands/KillAllCommand.java | 14 +- .../commands/KillCharacterCommand.java | 2 +- .../command/commands/PermissionCommand.java | 4 +- .../command/commands/PositionCommand.java | 9 +- .../command/commands/ReloadCommand.java | 8 +- .../command/commands/ResetConstCommand.java | 9 +- .../commands/ResetShopLimitCommand.java | 2 +- .../command/commands/SendMailCommand.java | 67 ++-- .../command/commands/SendMessageCommand.java | 4 +- .../commands/SetFetterLevelCommand.java | 2 +- .../command/commands/SetStatsCommand.java | 106 +++---- .../commands/SetWorldLevelCommand.java | 2 +- .../command/commands/SpawnCommand.java | 2 +- .../command/commands/TalentCommand.java | 88 +++--- .../command/commands/TeleportAllCommand.java | 2 +- .../command/commands/TeleportCommand.java | 2 +- .../command/commands/WeatherCommand.java | 2 +- .../grasscutter/game/shop/ShopManager.java | 12 +- .../grasscutter/plugin/api/PlayerHook.java | 1 + .../dispatch/DispatchHttpJsonHandler.java | 7 +- .../server/dispatch/DispatchServer.java | 32 +- .../grasscutter/server/game/GameServer.java | 15 +- .../grasscutter/server/game/GameSession.java | 12 +- .../java/emu/grasscutter/tools/Tools.java | 3 + .../java/emu/grasscutter/utils/Utils.java | 50 ++- src/main/resources/languages/en_US.json | 295 ++++++++++++++++++ 44 files changed, 687 insertions(+), 334 deletions(-) create mode 100644 src/main/resources/languages/en_US.json diff --git a/.gitignore b/.gitignore index 239309c12..32987345b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,4 @@ mitmdump.exe !lib/*.jar mongod.exe /src/generated/ -/*.sh -language/ -languages/ +/*.sh \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index dd24ac8a0..cf0a54a43 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -6,7 +6,6 @@ import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.game.mail.Mail; public final class Config { - public String DatabaseUrl = "mongodb://localhost:27017"; public String DatabaseCollection = "grasscutter"; diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index f17fb0443..07deb84fd 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -8,6 +8,8 @@ import org.reflections.Reflections; import java.util.*; +import static emu.grasscutter.utils.Language.translate; + @SuppressWarnings({"UnusedReturnValue", "unused"}) public final class CommandMap { private final Map<String, CommandHandler> commands = new HashMap<>(); @@ -109,7 +111,7 @@ public final class CommandMap { public void invoke(Player player, Player targetPlayer, String rawMessage) { rawMessage = rawMessage.trim(); if (rawMessage.length() == 0) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().No_command_specified); + CommandHandler.sendMessage(player, translate("commands.generic.not_specified")); return; } @@ -118,11 +120,12 @@ public final class CommandMap { List<String> args = new LinkedList<>(Arrays.asList(split)); String label = args.remove(0); String playerId = (player == null) ? consoleId : player.getAccount().getId(); - // Check for special cases - currently only target command + + // Check for special cases - currently only target command. String targetUidStr = null; - if (label.startsWith("@")) { // @[UID] + if (label.startsWith("@")) { // @[UID] targetUidStr = label.substring(1); - } else if (label.equalsIgnoreCase("target")) { // target [[@]UID] + } else if (label.equalsIgnoreCase("target")) { // target [[@]UID] if (args.size() > 0) { targetUidStr = args.get(0); if (targetUidStr.startsWith("@")) { @@ -133,68 +136,64 @@ public final class CommandMap { } } if (targetUidStr != null) { - if (targetUidStr.equals("")) { // Clears default targetPlayer + if (targetUidStr.equals("")) { // Clears the default targetPlayer. targetPlayerIds.remove(playerId); - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Target_cleared); - return; - } else { // Sets default targetPlayer to the UID given + CommandHandler.sendMessage(player, translate("commands.execution.clear_target")); + } else { // Sets default targetPlayer to the UID provided. try { int uid = Integer.parseInt(targetUidStr); targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid); if (targetPlayer == null) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Player_not_found_or_offline); + CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); } else { targetPlayerIds.put(playerId, uid); - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Target_set.replace("{uid}", targetUidStr)); + CommandHandler.sendMessage(player, translate("commands.execution.set_target", targetUidStr)); } } catch (NumberFormatException e) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Invalid_UID); + CommandHandler.sendMessage(player, translate("commands.execution.uid_error")); } - return; } + return; } // Get command handler. CommandHandler handler = this.commands.get(label); if (handler == null) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Unknown_command + label); + CommandHandler.sendMessage(player, translate("commands.generic.unknown_command", label)); return; } - // If any @UID argument is present, override targetPlayer with it + // If any @UID argument is present, override targetPlayer with it. for (int i = 0; i < args.size(); i++) { String arg = args.get(i); - if (!arg.startsWith("@")) { - continue; - } else { + if (arg.startsWith("@")) { arg = args.remove(i).substring(1); try { int uid = Integer.parseInt(arg); targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid); if (targetPlayer == null) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Player_not_found_or_offline); + CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); return; } break; } catch (NumberFormatException e) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Invalid_UID); + CommandHandler.sendMessage(player, translate("commands.execution.uid_error")); return; } } } + // If there's still no targetPlayer at this point, use previously-set target if (targetPlayer == null) { if (targetPlayerIds.containsKey(playerId)) { targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId)); // We check every time in case the target goes offline after being targeted if (targetPlayer == null) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Player_not_found_or_offline); + CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); return; } } else { - // If there's still no targetPlayer at this point, use local player - if (targetPlayer == null) { - targetPlayer = player; - } + // If there's still no targetPlayer at this point, use executor. + targetPlayer = player; } } @@ -205,12 +204,12 @@ public final class CommandMap { Account account = player.getAccount(); if (player != targetPlayer) { // Additional permission required for targeting another player if (!permissionNodeTargeted.isEmpty() && !account.hasPermission(permissionNodeTargeted)) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().You_not_permission_run_command); + CommandHandler.sendMessage(player, translate("commands.generic.permission_error")); return; } } if (!permissionNode.isEmpty() && !account.hasPermission(permissionNode)) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().You_not_permission_run_command); + CommandHandler.sendMessage(player, translate("commands.generic.permission_error")); return; } } @@ -220,10 +219,8 @@ public final class CommandMap { final Player targetPlayerF = targetPlayer; // Is there a better way to do this? Runnable runnable = () -> handler.execute(player, targetPlayerF, args); if(threading) { - Thread command = new Thread(runnable); - command.start(); - } - else { + new Thread(runnable).start(); + } else { runnable.run(); } } diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index a26e52ff0..627f4680f 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.database.DatabaseHelper; @@ -8,18 +7,20 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "account", usage = "account <create|delete> <username> [uid]", description = "Modify user accounts") public final class AccountCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender != null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().This_command_can_only_run_from_console); + CommandHandler.sendMessage(sender, translate("commands.generic.console_execute_error")); return; } if (args.size() < 2) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_command_usage); + CommandHandler.sendMessage(null, translate("commands.account.command_usage")); return; } @@ -28,7 +29,7 @@ public final class AccountCommand implements CommandHandler { switch (action) { default: - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_command_usage); + CommandHandler.sendMessage(null, translate("commands.account.command_usage")); return; case "create": int uid = 0; @@ -36,27 +37,27 @@ public final class AccountCommand implements CommandHandler { try { uid = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Invalid_UID); + CommandHandler.sendMessage(null, translate("commands.account.invalid")); return; } } emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithId(username, uid); if (account == null) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_exists); + CommandHandler.sendMessage(null, translate("commands.account.exists")); return; } else { account.addPermission("*"); account.save(); // Save account to database. - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_create_UID.replace("{uid}", Integer.toString(account.getPlayerUid()))); + CommandHandler.sendMessage(null, translate("commands.account.create", Integer.toString(account.getPlayerUid()))); } return; case "delete": if (DatabaseHelper.deleteAccount(username)) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_delete); + CommandHandler.sendMessage(null, translate("commands.account.delete")); } else { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Account_not_find); + CommandHandler.sendMessage(null, translate("commands.account.no_account")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java index b6eeac422..1aa234919 100644 --- a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "broadcast", usage = "broadcast <message>", description = "Sends a message to all the players", aliases = {"b"}, permission = "server.broadcast") public final class BroadcastCommand implements CommandHandler { @@ -14,7 +16,7 @@ public final class BroadcastCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (args.size() < 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Broadcast_command_usage); + CommandHandler.sendMessage(sender, translate("commands.broadcast.command_usage")); return; } @@ -24,6 +26,6 @@ public final class BroadcastCommand implements CommandHandler { CommandHandler.sendMessage(p, message); } - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Broadcast_message_sent); + CommandHandler.sendMessage(sender, translate("commands.broadcast.message_sent")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index fb1694325..1a4e97927 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -1,43 +1,43 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "changescene", usage = "changescene <scene id>", description = "Changes your scene", aliases = {"scene"}, permission = "player.changescene") public final class ChangeSceneCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (args.size() != 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Change_screen_usage); + CommandHandler.sendMessage(sender, translate("commands.changescene.usage")); return; } try { int sceneId = Integer.parseInt(args.get(0)); - if (sceneId == targetPlayer.getSceneId()) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Change_screen_you_in_that_screen); + CommandHandler.sendMessage(sender, translate("commands.changescene.already_in_scene")); return; } boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, targetPlayer.getPos()); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Change_screen + sceneId); + CommandHandler.sendMessage(sender, translate("commands.changescene.result", Integer.toString(sceneId))); if (!result) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Change_screen_not_exist); + CommandHandler.sendMessage(sender, translate("commands.changescene.exists_error")); } } catch (Exception e) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java index 2d0d0c48a..47d9f2c0d 100644 --- a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java @@ -10,6 +10,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "clear", usage = "clear <all|wp|art|mat>", //Merged /clearartifacts and /clearweapons to /clear <args> [uid] description = "Deletes unequipped unlocked items, including yellow rarity ones from your inventory", aliases = {"clear"}, permission = "player.clearinv") @@ -19,11 +21,11 @@ public final class ClearCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (args.size() < 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_usage); + CommandHandler.sendMessage(sender, translate("commands.clear.command_usage")); return; } Inventory playerInventory = targetPlayer.getInventory(); @@ -35,7 +37,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getItemType() == ItemType.ITEM_WEAPON) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_weapons.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.weapons", targetPlayer.getNickname())); } case "art" -> { toDelete = playerInventory.getItems().values().stream() @@ -43,7 +45,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getLevel() == 1 && item.getExp() == 0) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_artifacts.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.artifacts", targetPlayer.getNickname())); } case "mat" -> { toDelete = playerInventory.getItems().values().stream() @@ -51,7 +53,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getLevel() == 1 && item.getExp() == 0) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_artifacts.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.materials", targetPlayer.getNickname())); } case "all" -> { toDelete = playerInventory.getItems().values().stream() @@ -59,34 +61,44 @@ public final class ClearCommand implements CommandHandler { .filter(item1 -> item1.getLevel() == 1 && item1.getExp() == 0) .filter(item1 -> !item1.isLocked() && !item1.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_artifacts.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.artifacts", targetPlayer.getNickname())); + playerInventory.removeItems(toDelete); + toDelete = playerInventory.getItems().values().stream() .filter(item2 -> item2.getItemType() == ItemType.ITEM_MATERIAL) .filter(item2 -> !item2.isLocked() && !item2.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_artifacts.replace("{name}", targetPlayer.getNickname())); + playerInventory.removeItems(toDelete); + CommandHandler.sendMessage(sender, translate("commands.clear.materials", targetPlayer.getNickname())); + toDelete = playerInventory.getItems().values().stream() .filter(item3 -> item3.getItemType() == ItemType.ITEM_WEAPON) .filter(item3 -> item3.getLevel() == 1 && item3.getExp() == 0) .filter(item3 -> !item3.isLocked() && !item3.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_artifacts.replace("{name}", targetPlayer.getNickname())); + playerInventory.removeItems(toDelete); + CommandHandler.sendMessage(sender, translate("commands.clear.weapons", targetPlayer.getNickname())); + toDelete = playerInventory.getItems().values().stream() .filter(item4 -> item4.getItemType() == ItemType.ITEM_FURNITURE) .filter(item4 -> !item4.isLocked() && !item4.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_furniture.replace("{name}", targetPlayer.getNickname())); + playerInventory.removeItems(toDelete); + CommandHandler.sendMessage(sender, translate("commands.clear.furniture", targetPlayer.getNickname())); + toDelete = playerInventory.getItems().values().stream() .filter(item5 -> item5.getItemType() == ItemType.ITEM_DISPLAY) .filter(item5 -> !item5.isLocked() && !item5.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_displays.replace("{name}", targetPlayer.getNickname())); + playerInventory.removeItems(toDelete); + CommandHandler.sendMessage(sender, translate("commands.clear.displays", targetPlayer.getNickname())); + toDelete = playerInventory.getItems().values().stream() .filter(item6 -> item6.getItemType() == ItemType.ITEM_VIRTUAL) .filter(item6 -> !item6.isLocked() && !item6.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_virtuals.replace("{name}", targetPlayer.getNickname())); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Clear_everything.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.virtuals", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.clear.everything", targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index fff548d95..d41805c82 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -13,7 +13,7 @@ public final class CoopCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java index 29c5f10c6..a33a32603 100644 --- a/src/main/java/emu/grasscutter/command/commands/DropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -11,6 +11,8 @@ import emu.grasscutter.utils.Position; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "drop", usage = "drop <itemId|itemName> [amount]", description = "Drops an item near you", aliases = {"d", "dropitem"}, permission = "server.drop") public final class DropCommand implements CommandHandler { @@ -18,7 +20,7 @@ public final class DropCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(null, translate("commands.execution.need_target")); return; } @@ -30,25 +32,25 @@ public final class DropCommand implements CommandHandler { try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_amount); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); return; - } // Slightly cheeky here: no break so it falls through to initialize the first argument too + } // Slightly cheeky here: no break, so it falls through to initialize the first argument too case 1: try { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_id); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); return; } break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Drop_usage); + CommandHandler.sendMessage(sender, translate("commands.drop.command_usage")); return; } ItemData itemData = GameData.getItemDataMap().get(item); if (itemData == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_id); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); return; } if (itemData.isEquip()) { @@ -62,6 +64,6 @@ public final class DropCommand implements CommandHandler { EntityItem entity = new EntityItem(targetPlayer.getScene(), targetPlayer, itemData, targetPlayer.getPos().clone().addY(3f), amount); targetPlayer.getScene().addEntity(entity); } - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Drop_dropped_of.replace("{amount}", Integer.toString(amount)).replace("{item}", Integer.toString(item))); + CommandHandler.sendMessage(sender, translate("commands.drop.success", Integer.toString(amount), Integer.toString(item))); } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java index a52787b68..434e80c8f 100644 --- a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java @@ -1,43 +1,43 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", description = "Enter a dungeon", aliases = {"dungeon"}, permission = "player.enterdungeon") public final class EnterDungeonCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(null, translate("commands.execution.need_target")); return; } if (args.size() < 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().EnterDungeon_usage); + CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.usage")); return; } try { int dungeonId = Integer.parseInt(args.get(0)); - if (dungeonId == targetPlayer.getSceneId()) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().EnterDungeon_you_in_that_dungeon); + CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.in_dungeon_error")); return; } boolean result = targetPlayer.getServer().getDungeonManager().enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().EnterDungeon_changed_to_dungeon + dungeonId); + CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.changed", dungeonId)); if (!result) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().EnterDungeon_dungeon_not_found); + CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.not_found_error")); } } catch (Exception e) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().EnterDungeon_usage); + CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.usage")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index 21352dba5..ea249af9e 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -20,7 +20,7 @@ public final class GiveAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } int amount = 99999; diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 4d123dc5a..5b8351a5a 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -19,7 +19,7 @@ public final class GiveArtifactCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 6283a8e78..87c2d61e2 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -10,6 +10,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "givechar", usage = "givechar <avatarId> [level]", description = "Gives the player a specified character", aliases = {"givec"}, permission = "player.givechar") public final class GiveCharCommand implements CommandHandler { @@ -17,7 +19,7 @@ public final class GiveCharCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } @@ -30,7 +32,7 @@ public final class GiveCharCommand implements CommandHandler { level = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_avatar_level); + CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarLevel")); return; } // Cheeky fall-through to parse first argument too case 1: @@ -38,24 +40,24 @@ public final class GiveCharCommand implements CommandHandler { avatarId = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_avatar_id); + CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarId")); return; } break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveChar_usage); + CommandHandler.sendMessage(sender, translate("commands.giveChar.usage")); return; } AvatarData avatarData = GameData.getAvatarDataMap().get(avatarId); if (avatarData == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_avatar_id); + CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarId")); return; } // Check level. if (level > 90) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_avatar_level); + CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarLevel")); return; } @@ -75,6 +77,6 @@ public final class GiveCharCommand implements CommandHandler { avatar.recalcStats(); targetPlayer.addAvatar(avatar); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveChar_given.replace("{avatarId}", Integer.toString(avatarId)).replace("{level}", Integer.toString(level)).replace("{target}", Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate("commands.execution.giveChar.given", Integer.toString(avatarId), Integer.toString(level), Integer.toString(targetPlayer.getUid()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index f3f2adc17..2f020f7b3 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.regex.Pattern; import java.util.regex.Matcher; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", description = "Gives an item to you or the specified player", aliases = { "g", "item", "giveitem"}, permission = "player.give") public final class GiveCommand implements CommandHandler { @@ -33,7 +35,7 @@ public final class GiveCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } int item; @@ -67,21 +69,21 @@ public final class GiveCommand implements CommandHandler { try { refinement = Integer.parseInt(args.get(3)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_refinement); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemRefinement")); return; } // Fallthrough case 3: // <itemId|itemName> [amount] [level] try { lvl = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_level); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemLevel")); return; } // Fallthrough case 2: // <itemId|itemName> [amount] try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_amount); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); return; } // Fallthrough case 1: // <itemId|itemName> @@ -89,30 +91,28 @@ public final class GiveCommand implements CommandHandler { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_id); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); return; } break; default: // *No args* - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_usage); + CommandHandler.sendMessage(sender, translate("commands.give.usage")); return; } - - ItemData itemData = GameData.getItemDataMap().get(item); if (itemData == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_id); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); return; } if (refinement != 0) { if (itemData.getItemType() == ItemType.ITEM_WEAPON) { if (refinement < 1 || refinement > 5) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_refinement_must_between_1_and_5); + CommandHandler.sendMessage(sender, translate("commands.give.refinement_must_between_1_and_5")); return; } } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_refinement_only_applicable_weapons); + CommandHandler.sendMessage(sender, translate("commands.give.refinement_only_applicable_weapons")); return; } } @@ -120,11 +120,11 @@ public final class GiveCommand implements CommandHandler { this.item(targetPlayer, itemData, amount, lvl, refinement); if (!itemData.isEquip()) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_given.replace("{amount}", Integer.toString(amount)).replace("{item}", Integer.toString(item)).replace("{target}", Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate("commands.give.given", Integer.toString(amount), Integer.toString(item), Integer.toString(targetPlayer.getUid()))); } else if (itemData.getItemType() == ItemType.ITEM_WEAPON) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_given_with_level_and_refinement.replace("{item}", Integer.toString(item)).replace("{lvl}", Integer.toString(lvl)).replace("{refinement}", Integer.toString(refinement)).replace("{amount}", Integer.toString(amount)).replace("{target}", Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate("commands.give.given_with_level_and_refinement", Integer.toString(item), Integer.toString(lvl), Integer.toString(refinement), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); } else { - CommandHandler.sendMessage(sender,Grasscutter.getLanguage().Give_given_level.replace("{item}", Integer.toString(item)).replace("{lvl}", Integer.toString(lvl)).replace("{amount}", Integer.toString(amount))); + CommandHandler.sendMessage(sender, translate("commands.give.given_level", Integer.toString(item), Integer.toString(lvl), Integer.toString(amount))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java index 73ec8b032..9abebb8db 100644 --- a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "godmode", usage = "godmode [on|off|toggle]", description = "Prevents you from taking damage. Defaults to toggle.", permission = "player.godmode") public final class GodModeCommand implements CommandHandler { @@ -14,7 +16,7 @@ public final class GodModeCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } @@ -30,11 +32,11 @@ public final class GodModeCommand implements CommandHandler { case "toggle": break; // Already toggled default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Godmode_status); + break; } } targetPlayer.setGodmode(enabled); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Godmode_status.replace("{status}", (enabled ? Grasscutter.getLanguage().Enabled : Grasscutter.getLanguage().Disabled)).replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.godmode.success", (enabled ? translate("commands.status.enabled") : translate("commands.status.disabled")), targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java index e61cfd98f..bb0b861b0 100644 --- a/src/main/java/emu/grasscutter/command/commands/HealCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; @@ -10,13 +9,15 @@ import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "heal", usage = "heal|h", aliases = {"h"}, description = "Heal all characters in your current team.", permission = "player.heal") public final class HealCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } @@ -31,6 +32,6 @@ public final class HealCommand implements CommandHandler { entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); } }); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Heal_message); + CommandHandler.sendMessage(sender, translate("commands.heal.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java index 8397eae3e..93ac831b3 100644 --- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -8,6 +8,8 @@ import emu.grasscutter.game.player.Player; import java.util.*; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "help", usage = "help [command]", description = "Sends the help message or shows information about a specified command") public final class HelpCommand implements CommandHandler { @@ -31,16 +33,16 @@ public final class HelpCommand implements CommandHandler { } else { String command = args.get(0); CommandHandler handler = CommandMap.getInstance().getHandler(command); - StringBuilder builder = new StringBuilder(player == null ? "\n" + Grasscutter.getLanguage().Help + " - " : Grasscutter.getLanguage().Help + " - ").append(command).append(": \n"); + StringBuilder builder = new StringBuilder(player == null ? "\n" + translate("commands.status.help") + " - " : translate("commands.status.help") + " - ").append(command).append(": \n"); if (handler == null) { - builder.append(Grasscutter.getLanguage().No_command_found); + builder.append(translate("commands.generic.command_exist_error")); } else { Command annotation = handler.getClass().getAnnotation(Command.class); builder.append(" ").append(annotation.description()).append("\n"); - builder.append(Grasscutter.getLanguage().Help_usage).append(annotation.usage()); + builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(Grasscutter.getLanguage().Help_aliases); + builder.append("\n").append(translate("commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } @@ -56,13 +58,13 @@ public final class HelpCommand implements CommandHandler { void SendAllHelpMessage(Player player, List<Command> annotations) { if (player == null) { - StringBuilder builder = new StringBuilder("\n" + Grasscutter.getLanguage().Help_available_command + "\n"); + StringBuilder builder = new StringBuilder("\n" + translate("commands.help.available_commands") + "\n"); annotations.forEach(annotation -> { builder.append(annotation.label()).append("\n"); builder.append(" ").append(annotation.description()).append("\n"); - builder.append(Grasscutter.getLanguage().Help_usage).append(annotation.usage()); + builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(Grasscutter.getLanguage().Help_aliases); + builder.append("\n").append(translate("commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } @@ -73,13 +75,13 @@ public final class HelpCommand implements CommandHandler { CommandHandler.sendMessage(null, builder.toString()); } else { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().Help_available_command); + CommandHandler.sendMessage(player, translate("commands.help.available_commands")); annotations.forEach(annotation -> { StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); builder.append(" ").append(annotation.description()).append("\n"); - builder.append(Grasscutter.getLanguage().Help_usage).append(annotation.usage()); + builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(Grasscutter.getLanguage().Help_aliases); + builder.append("\n").append(translate("commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } diff --git a/src/main/java/emu/grasscutter/command/commands/KickCommand.java b/src/main/java/emu/grasscutter/command/commands/KickCommand.java index 0e3bb6c2b..270e28150 100644 --- a/src/main/java/emu/grasscutter/command/commands/KickCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KickCommand.java @@ -1,12 +1,13 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "kick", usage = "kick", description = "Kicks the specified player from the server (WIP)", permission = "server.kick") public final class KickCommand implements CommandHandler { @@ -14,14 +15,16 @@ public final class KickCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (sender != null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Kick_player_kick_player.replace("{sendUid}", Integer.toString(sender.getAccount().getPlayerUid())).replace("{sendName}", sender.getAccount().getUsername()).replace("kickUid", Integer.toString(targetPlayer.getUid())).replace("{kickName}", targetPlayer.getAccount().getUsername())); + CommandHandler.sendMessage(sender, translate("commands.kick.player_kick_player", + Integer.toString(sender.getAccount().getPlayerUid()), sender.getAccount().getUsername(), + Integer.toString(targetPlayer.getUid()), targetPlayer.getAccount().getUsername())); } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Kick_server_player.replace("{kickUid}", Integer.toString(targetPlayer.getUid())).replace("{kickName}", targetPlayer.getAccount().getUsername())); + CommandHandler.sendMessage(null, translate("commands.kick.server_kick_player", Integer.toString(targetPlayer.getUid()), 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 index 0e3660c1b..423c60bbd 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -10,6 +10,8 @@ import emu.grasscutter.game.world.Scene; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "killall", usage = "killall [sceneId]", description = "Kill all entities", permission = "server.killall") public final class KillAllCommand implements CommandHandler { @@ -17,7 +19,7 @@ public final class KillAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } @@ -30,14 +32,14 @@ public final class KillAllCommand implements CommandHandler { scene = targetPlayer.getWorld().getSceneById(Integer.parseInt(args.get(0))); break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Kill_usage); + CommandHandler.sendMessage(sender, translate("commands.kill.usage")); return; } } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); } if (scene == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Kill_scene_not_found_in_player_world); + CommandHandler.sendMessage(sender, translate("commands.kill.scene_not_found_in_player_world")); return; } @@ -46,7 +48,7 @@ public final class KillAllCommand implements CommandHandler { List<GameEntity> toKill = sceneF.getEntities().values().stream() .filter(entity -> entity instanceof EntityMonster) .toList(); - toKill.stream().forEach(entity -> sceneF.killEntity(entity, 0)); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Kill_kill_monsters_in_scene.replace("{size}", Integer.toString(toKill.size())).replace("{id}", Integer.toString(scene.getId()))); + toKill.forEach(entity -> sceneF.killEntity(entity, 0)); + CommandHandler.sendMessage(sender, translate("commands.kill.kill_monsters_in_scene", Integer.toString(toKill.size()), Integer.toString(scene.getId()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 7dc01363d..82a18f72d 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -19,7 +19,7 @@ public final class KillCharacterCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java index 6e041f338..aa99939f6 100644 --- a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -8,6 +8,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "permission", usage = "permission <add|remove> <permission>", description = "Grants or removes a permission for a user", permission = "*") public final class PermissionCommand implements CommandHandler { @@ -15,7 +17,7 @@ public final class PermissionCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java index 0e015d178..7f6548c5b 100644 --- a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; @@ -8,6 +7,8 @@ import emu.grasscutter.utils.Position; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "position", usage = "position", aliases = {"pos"}, description = "Get coordinates.") public final class PositionCommand implements CommandHandler { @@ -15,11 +16,13 @@ public final class PositionCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } Position pos = targetPlayer.getPos(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Position_message.replace("{x}", Float.toString(pos.getX())).replace("{y}", Float.toString(pos.getY())).replace("{z}", Float.toString(pos.getZ())).replace("{id}", Integer.toString(targetPlayer.getSceneId()))); + CommandHandler.sendMessage(sender, translate("commands.position.success", + Float.toString(pos.getX()), Float.toString(pos.getY()), Float.toString(pos.getZ()), + Integer.toString(targetPlayer.getSceneId()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 0e45c4efc..6c85d2024 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -7,19 +7,23 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "reload", usage = "reload", description = "Reload server config", permission = "server.reload") public final class ReloadCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Reload_reload_start); + CommandHandler.sendMessage(sender, translate("commands.reload.reload_start")); + Grasscutter.loadConfig(); Grasscutter.loadLanguage(); Grasscutter.getGameServer().getGachaManager().load(); Grasscutter.getGameServer().getDropManager().load(); Grasscutter.getGameServer().getShopManager().load(); Grasscutter.getDispatchServer().loadQueries(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Reload_reload_done); + + CommandHandler.sendMessage(sender, translate("commands.reload.reload_done")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java index 52ed0a55e..706fb95e0 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.avatar.Avatar; @@ -9,6 +8,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @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") @@ -17,13 +18,13 @@ public final class ResetConstCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (args.size() > 0 && args.get(0).equalsIgnoreCase("all")) { targetPlayer.getAvatars().forEach(this::resetConstellation); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().ResetConst_reset_all); + CommandHandler.sendMessage(sender, translate("commands.resetConst.reset_all")); } else { EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity(); if (entity == null) { @@ -33,7 +34,7 @@ public final class ResetConstCommand implements CommandHandler { Avatar avatar = entity.getAvatar(); this.resetConstellation(avatar); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().ResetConst_reset_all_done.replace("{name}", avatar.getAvatarData().getName())); + CommandHandler.sendMessage(sender, translate("commands.resetConst.success", avatar.getAvatarData().getName())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index f2cbf3476..59d8b32e8 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -13,7 +13,7 @@ public final class ResetShopLimitCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java index da0414f79..838bea567 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java @@ -10,6 +10,9 @@ import emu.grasscutter.game.player.Player; import java.util.HashMap; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + +@SuppressWarnings("ConstantConditions") @Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", description = "Sends mail to the specified user. The usage of this command changes based on it's composition state.", permission = "server.sendmail") public final class SendMailCommand implements CommandHandler { @@ -45,16 +48,16 @@ public final class SendMailCommand implements CommandHandler { if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) { mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail()); } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_user_not_exist.replace("{id}", args.get(0))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.user_not_exist", args.get(0))); return; } } } mailBeingConstructed.put(senderId, mailBuilder); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_start_composition); + CommandHandler.sendMessage(sender, translate("commands.sendMail.start_composition")); } - case 2 -> CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_templates); - default -> CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_invalid_arguments); + case 2 -> CommandHandler.sendMessage(sender, translate("commands.sendMail.templates")); + default -> CommandHandler.sendMessage(sender, translate("commands.sendMail.invalid_arguments")); } } else { MailBuilder mailBuilder = mailBeingConstructed.get(senderId); @@ -63,28 +66,28 @@ public final class SendMailCommand implements CommandHandler { switch (args.get(0).toLowerCase()) { case "stop" -> { mailBeingConstructed.remove(senderId); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_send_cancel); + CommandHandler.sendMessage(sender, translate("commands.sendMail.sendCancel")); return; } case "finish" -> { if (mailBuilder.constructionStage == 3) { if (!mailBuilder.sendToAll) { Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_send_done.replace("{name}", Integer.toString(mailBuilder.recipient))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.send_done", Integer.toString(mailBuilder.recipient))); } else { for (Player player : DatabaseHelper.getAllPlayers()) { Grasscutter.getGameServer().getPlayerByUid(player.getUid(), true).sendMail(mailBuilder.mail); } - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_send_all_done); + CommandHandler.sendMessage(sender, translate("commands.sendMail.send_all_done")); } mailBeingConstructed.remove(senderId); } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_not_composition_end.replace("{args}", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.not_composition_end", getConstructionArgs(mailBuilder.constructionStage))); } return; } case "help" -> { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_please_use.replace("{args}", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.please_use", getConstructionArgs(mailBuilder.constructionStage))); return; } default -> { @@ -92,19 +95,19 @@ public final class SendMailCommand implements CommandHandler { case 0 -> { String title = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.title = title; - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_set_title.replace("{title}", title)); + CommandHandler.sendMessage(sender, translate("commands.sendMail.set_title", title)); mailBuilder.constructionStage++; } case 1 -> { String contents = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.content = contents; - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_set_contents.replace("{contents}", contents)); + CommandHandler.sendMessage(sender, translate("commands.sendMail.set_contents", contents)); mailBuilder.constructionStage++; } case 2 -> { String msgSender = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.sender = msgSender; - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_set_message_sender.replace("{send}", msgSender)); + CommandHandler.sendMessage(sender, translate("commands.sendMail.set_message_sender", msgSender)); mailBuilder.constructionStage++; } case 3 -> { @@ -117,21 +120,21 @@ public final class SendMailCommand implements CommandHandler { try { refinement = Integer.parseInt(args.get(3)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_refinement); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemRefinement")); return; } // Fallthrough case 3: // <itemId|itemName> [amount] [level] try { lvl = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_level); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemLevel")); return; } // Fallthrough case 2: // <itemId|itemName> [amount] try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_amount); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); return; } // Fallthrough case 1: // <itemId|itemName> @@ -139,46 +142,34 @@ public final class SendMailCommand implements CommandHandler { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_item_id); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); return; } break; default: // *No args* - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Give_usage); + CommandHandler.sendMessage(sender, translate("commands.give.usage")); return; } mailBuilder.mail.itemList.add(new Mail.MailItem(item, amount, lvl)); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_send.replace("{amount}", Integer.toString(amount)).replace("{item}", Integer.toString(item)).replace("{lvl}", Integer.toString(lvl))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.send", Integer.toString(amount), Integer.toString(item), Integer.toString(lvl))); } } } } } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SendMail_invalid_arguments_please_use.replace("{args}", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate("commands.sendMail.invalid_arguments_please_use", getConstructionArgs(mailBuilder.constructionStage))); } } } private String getConstructionArgs(int stage) { - switch (stage) { - case 0 -> { - return Grasscutter.getLanguage().SendMail_title; - } - case 1 -> { - return Grasscutter.getLanguage().SendMail_message; - } - case 2 -> { - return Grasscutter.getLanguage().SendMail_sender; - - } - case 3 -> { - return Grasscutter.getLanguage().SendMail_arguments; - } - default -> { - Thread.dumpStack(); - return Grasscutter.getLanguage().SendMail_error.replace("{stage}", Integer.toString(stage)); - } - } + return switch(stage) { + case 0 -> translate("commands.sendMail.title"); + case 1 -> translate("commands.sendMail.message"); + case 2 -> translate("commands.sendMail.sender"); + case 3 -> translate("commands.sendMail.arguments"); + default -> translate("commands.sendMail.error", Integer.toString(stage)); + }; } public static class MailBuilder { diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index e04d3817e..41f76ba80 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "say", usage = "say <message>", description = "Sends a message to a player as the server", aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage") public final class SendMessageCommand implements CommandHandler { @@ -14,7 +16,7 @@ public final class SendMessageCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (args.size() == 0) { diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index f1768c72d..21507b998 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -18,7 +18,7 @@ public final class SetFetterLevelCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index 48dbe141f..3498e267b 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -10,13 +10,15 @@ import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.languages.Language; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Language; + +import static emu.grasscutter.utils.Language.translate; @Command(label = "setstats", usage = "setstats|stats <stat> <value>", description = "Set fight property for your current active character", aliases = {"stats"}, permission = "player.setstats") public final class SetStatsCommand implements CommandHandler { - class Stat { + static class Stat { String name; FightProperty prop; boolean percent; @@ -27,44 +29,44 @@ public final class SetStatsCommand implements CommandHandler { this.percent = percent; } } - Map<String, Stat> stats; + + Map<String, Stat> stats = new HashMap<>(); public SetStatsCommand() { Language lang = Grasscutter.getLanguage(); - stats = new HashMap<String, Stat>(); // Default stats - stats.put("maxhp", new Stat(lang.Stats_FIGHT_PROP_MAX_HP, FightProperty.FIGHT_PROP_MAX_HP, false)); - stats.put("hp", new Stat(lang.Stats_FIGHT_PROP_CUR_HP, FightProperty.FIGHT_PROP_CUR_HP, false)); - stats.put("atk", new Stat(lang.Stats_FIGHT_PROP_CUR_ATTACK, FightProperty.FIGHT_PROP_CUR_ATTACK, false)); - stats.put("atkb", new Stat(lang.Stats_FIGHT_PROP_BASE_ATTACK, FightProperty.FIGHT_PROP_BASE_ATTACK, false)); // This doesn't seem to get used to recalculate ATK, so it's only useful for stuff like Bennett's buff. - stats.put("def", new Stat(lang.Stats_FIGHT_PROP_DEFENSE, FightProperty.FIGHT_PROP_DEFENSE, false)); - stats.put("em", new Stat(lang.Stats_FIGHT_PROP_ELEMENT_MASTERY, FightProperty.FIGHT_PROP_ELEMENT_MASTERY, false)); - stats.put("er", new Stat(lang.Stats_FIGHT_PROP_CHARGE_EFFICIENCY, FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, true)); - stats.put("crate", new Stat(lang.Stats_FIGHT_PROP_CRITICAL, FightProperty.FIGHT_PROP_CRITICAL, true)); - stats.put("cdmg", new Stat(lang.Stats_FIGHT_PROP_CRITICAL_HURT, FightProperty.FIGHT_PROP_CRITICAL_HURT, true)); - stats.put("dmg", new Stat(lang.Stats_FIGHT_PROP_ADD_HURT, FightProperty.FIGHT_PROP_ADD_HURT, true)); // This seems to get reset after attacks - stats.put("eanemo", new Stat(lang.Stats_FIGHT_PROP_WIND_ADD_HURT, FightProperty.FIGHT_PROP_WIND_ADD_HURT, true)); - stats.put("ecryo", new Stat(lang.Stats_FIGHT_PROP_ICE_ADD_HURT, FightProperty.FIGHT_PROP_ICE_ADD_HURT, true)); - stats.put("edendro", new Stat(lang.Stats_FIGHT_PROP_GRASS_ADD_HURT, FightProperty.FIGHT_PROP_GRASS_ADD_HURT, true)); - stats.put("eelectro", new Stat(lang.Stats_FIGHT_PROP_ELEC_ADD_HURT, FightProperty.FIGHT_PROP_ELEC_ADD_HURT, true)); - stats.put("egeo", new Stat(lang.Stats_FIGHT_PROP_ROCK_ADD_HURT, FightProperty.FIGHT_PROP_ROCK_ADD_HURT, true)); - stats.put("ehydro", new Stat(lang.Stats_FIGHT_PROP_WATER_ADD_HURT, FightProperty.FIGHT_PROP_WATER_ADD_HURT, true)); - stats.put("epyro", new Stat(lang.Stats_FIGHT_PROP_FIRE_ADD_HURT, FightProperty.FIGHT_PROP_FIRE_ADD_HURT, true)); - stats.put("ephys", new Stat(lang.Stats_FIGHT_PROP_PHYSICAL_ADD_HURT, FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, true)); - stats.put("resall", new Stat(lang.Stats_FIGHT_PROP_SUB_HURT, FightProperty.FIGHT_PROP_SUB_HURT, true)); // This seems to get reset after attacks - stats.put("resanemo", new Stat(lang.Stats_FIGHT_PROP_WIND_SUB_HURT, FightProperty.FIGHT_PROP_WIND_SUB_HURT, true)); - stats.put("rescryo", new Stat(lang.Stats_FIGHT_PROP_ICE_SUB_HURT, FightProperty.FIGHT_PROP_ICE_SUB_HURT, true)); - stats.put("resdendro", new Stat(lang.Stats_FIGHT_PROP_GRASS_SUB_HURT, FightProperty.FIGHT_PROP_GRASS_SUB_HURT, true)); - stats.put("reselectro", new Stat(lang.Stats_FIGHT_PROP_ELEC_SUB_HURT, FightProperty.FIGHT_PROP_ELEC_SUB_HURT, true)); - stats.put("resgeo", new Stat(lang.Stats_FIGHT_PROP_ROCK_SUB_HURT, FightProperty.FIGHT_PROP_ROCK_SUB_HURT, true)); - stats.put("reshydro", new Stat(lang.Stats_FIGHT_PROP_WATER_SUB_HURT, FightProperty.FIGHT_PROP_WATER_SUB_HURT, true)); - stats.put("respyro", new Stat(lang.Stats_FIGHT_PROP_FIRE_SUB_HURT, FightProperty.FIGHT_PROP_FIRE_SUB_HURT, true)); - stats.put("resphys", new Stat(lang.Stats_FIGHT_PROP_PHYSICAL_SUB_HURT, FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, true)); - stats.put("cdr", new Stat(lang.Stats_FIGHT_PROP_SKILL_CD_MINUS_RATIO, FightProperty.FIGHT_PROP_SKILL_CD_MINUS_RATIO, true)); - stats.put("heal", new Stat(lang.Stats_FIGHT_PROP_HEAL_ADD, FightProperty.FIGHT_PROP_HEAL_ADD, true)); - stats.put("heali", new Stat(lang.Stats_FIGHT_PROP_HEALED_ADD, FightProperty.FIGHT_PROP_HEALED_ADD, true)); - stats.put("shield", new Stat(lang.Stats_FIGHT_PROP_SHIELD_COST_MINUS_RATIO, FightProperty.FIGHT_PROP_SHIELD_COST_MINUS_RATIO, true)); - stats.put("defi", new Stat(lang.Stats_FIGHT_PROP_DEFENCE_IGNORE_RATIO, FightProperty.FIGHT_PROP_DEFENCE_IGNORE_RATIO, true)); + stats.put("maxhp", new Stat(FightProperty.FIGHT_PROP_MAX_HP.toString(), FightProperty.FIGHT_PROP_MAX_HP, false)); + stats.put("hp", new Stat(FightProperty.FIGHT_PROP_CUR_HP.toString(), FightProperty.FIGHT_PROP_CUR_HP, false)); + stats.put("atk", new Stat(FightProperty.FIGHT_PROP_CUR_ATTACK.toString(), FightProperty.FIGHT_PROP_CUR_ATTACK, false)); + stats.put("atkb", new Stat(FightProperty.FIGHT_PROP_BASE_ATTACK.toString(), FightProperty.FIGHT_PROP_BASE_ATTACK, false)); // This doesn't seem to get used to recalculate ATK, so it's only useful for stuff like Bennett's buff. + stats.put("def", new Stat(FightProperty.FIGHT_PROP_DEFENSE.toString(), FightProperty.FIGHT_PROP_DEFENSE, false)); + stats.put("em", new Stat(FightProperty.FIGHT_PROP_ELEMENT_MASTERY.toString(), FightProperty.FIGHT_PROP_ELEMENT_MASTERY, false)); + stats.put("er", new Stat(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY.toString(), FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, true)); + stats.put("crate", new Stat(FightProperty.FIGHT_PROP_CRITICAL.toString(), FightProperty.FIGHT_PROP_CRITICAL, true)); + stats.put("cdmg", new Stat(FightProperty.FIGHT_PROP_CRITICAL_HURT.toString(), FightProperty.FIGHT_PROP_CRITICAL_HURT, true)); + stats.put("dmg", new Stat(FightProperty.FIGHT_PROP_ADD_HURT.toString(), FightProperty.FIGHT_PROP_ADD_HURT, true)); // This seems to get reset after attacks + stats.put("eanemo", new Stat(FightProperty.FIGHT_PROP_WIND_ADD_HURT.toString(), FightProperty.FIGHT_PROP_WIND_ADD_HURT, true)); + stats.put("ecryo", new Stat(FightProperty.FIGHT_PROP_ICE_ADD_HURT.toString(), FightProperty.FIGHT_PROP_ICE_ADD_HURT, true)); + stats.put("edendro", new Stat(FightProperty.FIGHT_PROP_GRASS_ADD_HURT.toString(), FightProperty.FIGHT_PROP_GRASS_ADD_HURT, true)); + stats.put("eelectro", new Stat(FightProperty.FIGHT_PROP_ELEC_ADD_HURT.toString(), FightProperty.FIGHT_PROP_ELEC_ADD_HURT, true)); + stats.put("egeo", new Stat(FightProperty.FIGHT_PROP_ROCK_ADD_HURT.toString(), FightProperty.FIGHT_PROP_ROCK_ADD_HURT, true)); + stats.put("ehydro", new Stat(FightProperty.FIGHT_PROP_WATER_ADD_HURT.toString(), FightProperty.FIGHT_PROP_WATER_ADD_HURT, true)); + stats.put("epyro", new Stat(FightProperty.FIGHT_PROP_FIRE_ADD_HURT.toString(), FightProperty.FIGHT_PROP_FIRE_ADD_HURT, true)); + stats.put("ephys", new Stat(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT.toString(), FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, true)); + stats.put("resall", new Stat(FightProperty.FIGHT_PROP_SUB_HURT.toString(), FightProperty.FIGHT_PROP_SUB_HURT, true)); // This seems to get reset after attacks + stats.put("resanemo", new Stat(FightProperty.FIGHT_PROP_WIND_SUB_HURT.toString(), FightProperty.FIGHT_PROP_WIND_SUB_HURT, true)); + stats.put("rescryo", new Stat(FightProperty.FIGHT_PROP_ICE_SUB_HURT.toString(), FightProperty.FIGHT_PROP_ICE_SUB_HURT, true)); + stats.put("resdendro", new Stat(FightProperty.FIGHT_PROP_GRASS_SUB_HURT.toString(), FightProperty.FIGHT_PROP_GRASS_SUB_HURT, true)); + stats.put("reselectro", new Stat(FightProperty.FIGHT_PROP_ELEC_SUB_HURT.toString(), FightProperty.FIGHT_PROP_ELEC_SUB_HURT, true)); + stats.put("resgeo", new Stat(FightProperty.FIGHT_PROP_ROCK_SUB_HURT.toString(), FightProperty.FIGHT_PROP_ROCK_SUB_HURT, true)); + stats.put("reshydro", new Stat(FightProperty.FIGHT_PROP_WATER_SUB_HURT.toString(), FightProperty.FIGHT_PROP_WATER_SUB_HURT, true)); + stats.put("respyro", new Stat(FightProperty.FIGHT_PROP_FIRE_SUB_HURT.toString(), FightProperty.FIGHT_PROP_FIRE_SUB_HURT, true)); + stats.put("resphys", new Stat(FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT.toString(), FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, true)); + stats.put("cdr", new Stat(FightProperty.FIGHT_PROP_SKILL_CD_MINUS_RATIO.toString(), FightProperty.FIGHT_PROP_SKILL_CD_MINUS_RATIO, true)); + stats.put("heal", new Stat(FightProperty.FIGHT_PROP_HEAL_ADD.toString(), FightProperty.FIGHT_PROP_HEAL_ADD, true)); + stats.put("heali", new Stat(FightProperty.FIGHT_PROP_HEALED_ADD.toString(), FightProperty.FIGHT_PROP_HEALED_ADD, true)); + stats.put("shield", new Stat(FightProperty.FIGHT_PROP_SHIELD_COST_MINUS_RATIO.toString(), FightProperty.FIGHT_PROP_SHIELD_COST_MINUS_RATIO, true)); + stats.put("defi", new Stat(FightProperty.FIGHT_PROP_DEFENCE_IGNORE_RATIO.toString(), FightProperty.FIGHT_PROP_DEFENCE_IGNORE_RATIO, true)); // Compatibility aliases stats.put("mhp", stats.get("maxhp")); stats.put("cr", stats.get("crate")); @@ -175,26 +177,23 @@ public final class SetStatsCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - Language lang = Grasscutter.getLanguage(); - String syntax = sender == null ? lang.SetStats_usage_console : lang.SetStats_usage_console; - String usage = syntax + lang.SetStats_help_message; + String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.ingame"); + String usage = syntax + translate("commands.setStats.help_message"); String statStr; String valueStr; if (targetPlayer == null) { - CommandHandler.sendMessage(sender, lang.Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } - switch (args.size()) { - default: - CommandHandler.sendMessage(sender, usage); - return; - case 2: - statStr = args.get(0).toLowerCase(); - valueStr = args.get(1); - break; - }; + if (args.size() == 2) { + statStr = args.get(0).toLowerCase(); + valueStr = args.get(1); + } else { + CommandHandler.sendMessage(sender, usage); + return; + } EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity(); @@ -206,7 +205,7 @@ public final class SetStatsCommand implements CommandHandler { value = Float.parseFloat(valueStr); } } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, lang.SetStats_value_error); + CommandHandler.sendMessage(sender, translate("commands.setStats.value_error")); return; } @@ -220,15 +219,14 @@ public final class SetStatsCommand implements CommandHandler { valueStr = String.format("%.0f", value); } if (targetPlayer == sender) { - CommandHandler.sendMessage(sender, lang.SetStats_set_self.replace("{name}", stat.name).replace("{value}", valueStr)); + CommandHandler.sendMessage(sender, translate("commands.setStats.set_self", stat.name, valueStr)); } else { String uidStr = targetPlayer.getAccount().getId(); - CommandHandler.sendMessage(sender, lang.SetStats_set_for_uid.replace("{name}", stat.name).replace("{uid}", uidStr).replace("{value}", valueStr)); + CommandHandler.sendMessage(sender, translate("commands.setStats.set_self", stat.name, uidStr, valueStr)); } - return; } else { CommandHandler.sendMessage(sender, usage); - return; } + return; } } diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index cd53237ca..39f4753a7 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -15,7 +15,7 @@ public final class SetWorldLevelCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java index 48d6235cb..c50eb972d 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -27,7 +27,7 @@ public final class SpawnCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index 559006473..c9b2e8931 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -12,13 +12,15 @@ import emu.grasscutter.server.packet.send.PacketAvatarSkillUpgradeRsp; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "talent", usage = "talent <talentID> <value>", description = "Set talent level for your current active character", permission = "player.settalent") public final class TalentCommand implements CommandHandler { private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) { int oldLevel = avatar.getSkillLevelMap().get(talentId); if (talentLevel < 0 || talentLevel > 15) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_lower_16); + CommandHandler.sendMessage(sender, translate("commands.talent.lower_16")); return; } @@ -30,29 +32,29 @@ public final class TalentCommand implements CommandHandler { player.sendPacket(new PacketAvatarSkillChangeNotify(avatar, talentId, oldLevel, talentLevel)); player.sendPacket(new PacketAvatarSkillUpgradeRsp(avatar, talentId, oldLevel, talentLevel)); - String successMessage = Grasscutter.getLanguage().Talent_set_id.replace("{id}", Integer.toString(talentId)); + String successMessage = "commands.talent.set_id"; AvatarSkillDepotData depot = avatar.getData().getSkillDepot(); if (talentId == depot.getSkills().get(0)) { - successMessage = Grasscutter.getLanguage().Talent_set_atk; + successMessage = "commands.talent.set_atk"; } else if (talentId == depot.getSkills().get(1)) { - successMessage = Grasscutter.getLanguage().Talent_set_e; + successMessage = "commands.talent.set_e"; } else if (talentId == depot.getEnergySkill()) { - successMessage = Grasscutter.getLanguage().Talent_set_q; + successMessage = "commands.talent.set_q"; } - CommandHandler.sendMessage(sender, successMessage.replace("{level}", Integer.toString(talentLevel))); + CommandHandler.sendMessage(sender, translate(successMessage, talentLevel)); } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } if (args.size() < 1){ - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_1); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_2); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_3); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); return; } @@ -60,66 +62,54 @@ public final class TalentCommand implements CommandHandler { Avatar avatar = entity.getAvatar(); String cmdSwitch = args.get(0); switch (cmdSwitch) { - default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_1); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_2); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_3); - return; - case "set": - if (args.size() < 3){ - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_1); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_3); + default -> { + CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); + return; + } + case "set" -> { + if (args.size() < 3) { + CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); return; } - try { int skillId = Integer.parseInt(args.get(1)); int newLevel = Integer.parseInt(args.get(2)); setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_invalid_skill_id); + CommandHandler.sendMessage(sender, translate("commands.talent.invalid_skill_id")); return; } - break; - case "n": - case "e": - case "q": - if (args.size() < 2){ - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_usage_2); + } + case "n", "e", "q" -> { + if (args.size() < 2) { + CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); return; } - AvatarSkillDepotData SkillDepot = avatar.getData().getSkillDepot(); - int skillId; - switch (cmdSwitch) { - default: - skillId = SkillDepot.getSkills().get(0); - break; - case "e": - skillId = SkillDepot.getSkills().get(1); - break; - case "q": - skillId = SkillDepot.getEnergySkill(); - break; - } - + int skillId = switch (cmdSwitch) { + default -> SkillDepot.getSkills().get(0); + case "e" -> SkillDepot.getSkills().get(1); + case "q" -> SkillDepot.getEnergySkill(); + }; try { int newLevel = Integer.parseInt(args.get(1)); setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_invalid_talent_level); + CommandHandler.sendMessage(sender, translate("commands.talent.invalid_level")); return; } - break; - case "getid": + } + case "getid" -> { int skillIdNorAtk = avatar.getData().getSkillDepot().getSkills().get(0); int skillIdE = avatar.getData().getSkillDepot().getSkills().get(1); int skillIdQ = avatar.getData().getSkillDepot().getEnergySkill(); - - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_normal_attack_id.replace("{id}", Integer.toString(skillIdNorAtk))); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_e_skill_id.replace("{id}", Integer.toString(skillIdE))); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Talent_q_skill_id.replace("{id}", Integer.toString(skillIdQ))); - break; + CommandHandler.sendMessage(sender, translate("commands.talent.normal_attack_id", Integer.toString(skillIdNorAtk))); + CommandHandler.sendMessage(sender, translate("commands.talent.e_skill_id", Integer.toString(skillIdE))); + CommandHandler.sendMessage(sender, translate("commands.talent.q_skill_id", Integer.toString(skillIdQ))); + } } } } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index 4c9f2a9cd..fe9b5dc49 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -14,7 +14,7 @@ public final class TeleportAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index f5a533bef..ef7e68ae9 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -26,7 +26,7 @@ public final class TeleportCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index eaf73e53a..431e53853 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -16,7 +16,7 @@ public final class WeatherCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Target_needed); + CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index 7222db816..2c5d014f5 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -4,13 +4,7 @@ import com.google.gson.reflect.TypeToken; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.ShopGoodsData; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.managers.InventoryManager; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.net.proto.ItemParamOuterClass; -import emu.grasscutter.net.proto.ShopGoodsOuterClass; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -54,9 +48,9 @@ public class ShopManager { public static int getShopNextRefreshTime(ShopInfo shopInfo) { return switch (shopInfo.getShopRefreshType()) { - case SHOP_REFRESH_DAILY -> Utils.GetNextTimestampOfThisHour(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); - case SHOP_REFRESH_WEEKLY -> Utils.GetNextTimestampOfThisHourInNextWeek(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); - case SHOP_REFRESH_MONTHLY -> Utils.GetNextTimestampOfThisHourInNextMonth(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + case SHOP_REFRESH_DAILY -> Utils.getNextTimestampOfThisHour(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + case SHOP_REFRESH_WEEKLY -> Utils.getNextTimestampOfThisHourInNextWeek(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + case SHOP_REFRESH_MONTHLY -> Utils.getNextTimestampOfThisHourInNextMonth(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); default -> 0; }; } diff --git a/src/main/java/emu/grasscutter/plugin/api/PlayerHook.java b/src/main/java/emu/grasscutter/plugin/api/PlayerHook.java index 3760ea42c..6b68b7622 100644 --- a/src/main/java/emu/grasscutter/plugin/api/PlayerHook.java +++ b/src/main/java/emu/grasscutter/plugin/api/PlayerHook.java @@ -28,6 +28,7 @@ public final class PlayerHook { /** * Kicks a player from the server. + * TODO: Refactor to kick using a packet. */ public void kick() { this.player.getSession().close(); diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index e1a07b412..54ee38988 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -2,6 +2,7 @@ package emu.grasscutter.server.dispatch; import java.io.IOException; import java.util.Arrays; +import java.util.Objects; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; @@ -9,6 +10,8 @@ import express.http.HttpContextHandler; import express.http.Request; import express.http.Response; +import static emu.grasscutter.utils.Language.translate; + public final class DispatchHttpJsonHandler implements HttpContextHandler { private final String response; private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer @@ -31,8 +34,8 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { @Override public void handle(Request req, Response res) throws IOException { // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled - if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> x == req.baseUrl())) { - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_request.replace("{ip}", req.ip()).replace("{method}", req.method()).replace("{url}", req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); + if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, req.baseUrl()))) { + Grasscutter.getLogger().info(translate("messages.dispatch.request", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); } res.send(response); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 196faf880..21ef3ef69 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -33,6 +33,8 @@ import java.io.*; import java.net.URLDecoder; import java.util.*; +import static emu.grasscutter.utils.Language.translate; + public final class DispatchServer { public static String query_region_list = ""; public static String query_cur_region = ""; @@ -211,21 +213,21 @@ public final class DispatchServer { sslContextFactory.setKeyStorePassword(Grasscutter.getConfig().getDispatchOptions().KeystorePassword); } catch (Exception e) { e.printStackTrace(); - Grasscutter.getLogger().warn(Grasscutter.getLanguage().Not_load_keystore); + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); sslContextFactory.setKeyStorePassword("123456"); - Grasscutter.getLogger().warn(Grasscutter.getLanguage().Use_default_keystore); + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password")); } catch (Exception e2) { - Grasscutter.getLogger().warn(Grasscutter.getLanguage().Load_keystore_error); + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error")); e2.printStackTrace(); } } serverConnector = new ServerConnector(server, sslContextFactory); } else { - Grasscutter.getLogger().warn(Grasscutter.getLanguage().Not_find_ssl_cert); + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); Grasscutter.getConfig().getDispatchOptions().UseSSL = false; serverConnector = new ServerConnector(server); @@ -245,11 +247,11 @@ public final class DispatchServer { } }); - httpServer.get("/", (req, res) -> res.send(Grasscutter.getLanguage().Welcome)); + httpServer.get("/", (req, res) -> res.send(translate("messages.status.welcome"))); httpServer.raw().error(404, ctx -> { if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { - Grasscutter.getLogger().info(Grasscutter.getLanguage().Potential_unhandled_request.replace("{method}", ctx.method()).replace("{url}", ctx.url())); + Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url())); } ctx.contentType("text/html"); ctx.result("<!doctype html><html lang=\"en\"><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything. @@ -307,7 +309,7 @@ public final class DispatchServer { if (requestData == null) { return; } - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_try_login.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", req.ip())); res.send(this.getAuthHandler().handleGameLogin(req, requestData)); }); @@ -327,7 +329,7 @@ public final class DispatchServer { return; } LoginResultJson responseData = new LoginResultJson(); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_login_token.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt")); // Login Account account = DatabaseHelper.getAccountById(requestData.uid); @@ -335,16 +337,16 @@ public final class DispatchServer { // Test if (account == null || !account.getSessionKey().equals(requestData.token)) { responseData.retcode = -111; - responseData.message = Grasscutter.getLanguage().Game_account_cache_error; + responseData.message = translate("messages.dispatch.account.account_cache_error"); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_token_login_failed.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", req.ip())); } 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(Grasscutter.getLanguage().Client_login_in_token.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", req.ip(), requestData.uid)); } res.send(responseData); @@ -374,16 +376,16 @@ public final class DispatchServer { // Test if (account == null || !account.getSessionKey().equals(loginData.token)) { responseData.retcode = -201; - responseData.message = Grasscutter.getLanguage().Wrong_session_key; + responseData.message = translate("messages.dispatch.account.session_key_error"); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_exchange_combo_token.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", req.ip())); } else { responseData.message = "OK"; responseData.data.open_id = loginData.uid; responseData.data.combo_id = "157795300"; responseData.data.combo_token = account.generateLoginToken(); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_exchange_combo_token.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", req.ip())); } res.send(responseData); @@ -449,7 +451,7 @@ public final class DispatchServer { httpServer.get("/gcstatic/*", new StaticFileHandler()); httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Dispatch_start_server_port.replace("{port}", Integer.toString(httpServer.raw().port()))); + Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(httpServer.raw().port()))); } private Map<String, String> parseQueryString(String qs) { diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index db4a8c32b..f414a69ff 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -29,6 +29,8 @@ import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import static emu.grasscutter.utils.Language.translate; + public final class GameServer extends KcpServer { private final InetSocketAddress address; private final GameServerPacketHandler packetHandler; @@ -48,6 +50,13 @@ public final class GameServer extends KcpServer { private final CombineManger combineManger; + public GameServer() { + this(new InetSocketAddress( + Grasscutter.getConfig().getGameServerOptions().Ip, + Grasscutter.getConfig().getGameServerOptions().Port + )); + } + public GameServer(InetSocketAddress address) { super(address); @@ -76,7 +85,7 @@ public final class GameServer extends KcpServer { try { onTick(); } catch (Exception e) { - Grasscutter.getLogger().error(Grasscutter.getLanguage().An_error_occurred_during_game_update, e); + Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e); } } }, new Date(), 1000L); @@ -214,8 +223,8 @@ public final class GameServer extends KcpServer { @Override public void onStartFinish() { - Grasscutter.getLogger().info(Grasscutter.getLanguage().Grasscutter_is_free); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Game_start_port.replace("{port}", Integer.toString(address.getPort()))); + Grasscutter.getLogger().info(translate("messages.status.free_software")); + Grasscutter.getLogger().info(translate("messages.game.port_bind", Integer.toString(address.getPort()))); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call(); } diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index ff024b03b..476f9bf4a 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -22,6 +22,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; +import static emu.grasscutter.utils.Language.translate; + public class GameSession extends KcpChannel { private GameServer server; @@ -113,21 +115,21 @@ public class GameSession extends KcpChannel { @Override protected void onConnect() { - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_connect.replace("{address}", getAddress().getHostString().toLowerCase())); + Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().getHostString().toLowerCase())); } @Override - protected synchronized void onDisconnect() { // Synchronize so we dont add character at the same time - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_disconnect.replace("{address}", getAddress().getHostString().toLowerCase())); + protected synchronized void onDisconnect() { // Synchronize so we don't add character at the same time. + Grasscutter.getLogger().info(translate("messages.game.disconnect", this.getAddress().getHostString().toLowerCase())); // Set state so no more packets can be handled this.setState(SessionState.INACTIVE); // Save after disconnecting if (this.isLoggedIn()) { - // Save + // Call logout event. getPlayer().onLogout(); - // Remove from gameserver + // Remove from server. getServer().getPlayers().remove(getPlayer().getUid()); } } diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index d7f5e986a..efd7b8f93 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -93,6 +93,7 @@ public final class Tools { } Grasscutter.getLogger().info("GM Handbook generated!"); + System.exit(0); } @SuppressWarnings("deprecation") @@ -184,6 +185,8 @@ public final class Tools { writer.println(",\"200\": \"Standard\", \"301\": \"Avatar Event\", \"302\": \"Weapon event\""); writer.println("}\n}"); } + Grasscutter.getLogger().info("Mappings generated!"); + System.exit(0); } } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 1b4a00b90..268cc6f40 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -15,6 +15,8 @@ import io.netty.buffer.Unpooled; import org.slf4j.Logger; +import static emu.grasscutter.utils.Language.translate; + @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) public final class Utils { public static final Random random = new Random(); @@ -176,15 +178,15 @@ public final class Utils { // Check for resources folder. if(!fileExists(resourcesFolder)) { - logger.info(Grasscutter.getLanguage().Create_resources_folder); - logger.info(Grasscutter.getLanguage().Place_copy); + logger.info(translate("messages.status.create_resources")); + logger.info(translate("messages.status.resources_error")); createFolder(resourcesFolder); exit = true; } - // Check for BinOutput + ExcelBinOuput. + // Check for BinOutput + ExcelBinOutput. if(!fileExists(resourcesFolder + "BinOutput") || !fileExists(resourcesFolder + "ExcelBinOutput")) { - logger.info(Grasscutter.getLanguage().Place_copy); + logger.info(translate("messages.status.resources_error")); exit = true; } @@ -195,7 +197,11 @@ public final class Utils { if(exit) System.exit(1); } - public static int GetNextTimestampOfThisHour(int hour, String timeZone, int param) { + /** + * Gets the timestamp of the next hour. + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHour(int hour, String timeZone, int param) { ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); for (int i = 0; i < param; i ++){ if (zonedDateTime.getHour() < hour) { @@ -204,10 +210,14 @@ public final class Utils { zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0); } } - return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); } - public static int GetNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { + /** + * Gets the timestamp of the next hour in a week. + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); for (int i = 0; i < param; i++) { if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) { @@ -216,10 +226,14 @@ public final class Utils { zonedDateTime = zonedDateTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(hour).withMinute(0).withSecond(0); } } - return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); } - public static int GetNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { + /** + * Gets the timestamp of the next hour in a month. + * @return The timestamp in UNIX seconds. + */ + public static int getNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); for (int i = 0; i < param; i++) { if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) { @@ -228,6 +242,22 @@ public final class Utils { zonedDateTime = zonedDateTime.with(TemporalAdjusters.firstDayOfNextMonth()).withHour(hour).withMinute(0).withSecond(0); } } - return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + return (int) zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + /** + * Retrieves a string from an input stream. + * @param stream The input stream. + * @return The string. + */ + public static String readFromInputStream(InputStream stream) { + StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + String line; while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } stream.close(); + } catch (IOException e) { + Grasscutter.getLogger().warn("Failed to read from input stream."); + } return stringBuilder.toString(); } } diff --git a/src/main/resources/languages/en_US.json b/src/main/resources/languages/en_US.json new file mode 100644 index 000000000..f2f6b59ec --- /dev/null +++ b/src/main/resources/languages/en_US.json @@ -0,0 +1,295 @@ +{ + "messages": { + "game": { + "port_bind": "Game Server started on port %s", + "connect": "Client connected from %s", + "disconnect": "Client disconnected from %s", + "game_update_error": "An error occurred during game update.", + "command_error": "Command error:" + }, + "dispatch": { + "port_bind": "[Dispatch] Dispatch server started on port %s", + "request": "[Dispatch] Client %s %s request: %s", + "keystore": { + "general_error": "[Dispatch] Error while loading keystore!", + "password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...", + "no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.", + "default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json." + }, + "no_commands_error": "Commands are not supported in dispatch only mode.", + "unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s", + "account": { + "login_attempt": "[Dispatch] Client %s is trying to log in", + "login_success": "[Dispatch] Client %s logged in as %s", + "login_token_attempt": "[Dispatch] Client %s is trying to log in via token", + "login_token_error": "[Dispatch] Client %s failed to log in via token", + "login_token_success": "[Dispatch] Client %s logged in via token as %s", + "combo_token_success": "[Dispatch] Client %s succeed to exchange combo token", + "combo_token_error": "[Dispatch] Client %s failed to exchange combo token", + "account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created", + "account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed", + "account_login_exist_error": "[Dispatch] Client %s failed to log in: Account no found", + "account_cache_error": "Game account cache information error", + "session_key_error": "Wrong session key.", + "username_error": "Username not found.", + "username_create_error": "Username not found, create failed." + } + }, + "status": { + "free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter", + "starting": "Starting Grasscutter...", + "shutdown": "Shutting down...", + "done": "Done! For help, type \"help\"", + "error": "An error occurred.", + "welcome": "Welcome to Grasscutter", + "run_mode_error": "Invalid server run mode: %s.", + "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", + "create_resources": "Creating resources folder...", + "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder." + } + }, + "commands": { + "generic": { + "not_specified": "No command specified.", + "unknown_command": "Unknown command: %s", + "permission_error": "You do not have permission to run this command.", + "console_execute_error": "This command can only be run from the console.", + "player_execute_error": "Run this command in-game.", + "command_exist_error": "No command found.", + "invalid": { + "amount": "Invalid amount.", + "artifactId": "Invalid artifactId.", + "avatarId": "Invalid avatarId.", + "avatarLevel": "Invalid avatarLevel.", + "entityId": "Invalid entityId.", + "itemId": "Invalid itemId.", + "itemLevel": "Invalid itemLevel.", + "itemRefinement": "Invalid itemRefinement.", + "playerId": "Invalid playerId.", + "uid": "Invalid UID." + } + }, + "execution": { + "uid_error": "Invalid UID.", + "player_exist_error": "Player not found.", + "player_offline_error": "Player is not online.", + "item_id_error": "Invalid item ID.", + "item_player_exist_error": "Invalid item or UID.", + "entity_id_error": "Invalid entity ID.", + "player_exist_offline_error": "Player not found or is not online.", + "argument_error": "Invalid arguments.", + "clear_target": "Target cleared.", + "set_target": "Subsequent commands will target @%s by default.", + "need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID." + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "help": "Help", + "success": "Success" + }, + "account": { + "modify": "Modify user accounts", + "invalid": "Invalid UID.", + "exists": "Account already exists.", + "create": "Account created with UID %s.", + "delete": "Account deleted.", + "no_account": "Account not found.", + "command_usage": "Usage: account <create|delete> <username> [uid]" + }, + "broadcast": { + "command_usage": "Usage: broadcast <message>", + "message_sent": "Message sent." + }, + "changescene": { + "usage": "Usage: changescene <sceneId>", + "already_in_scene": "You are already in that scene.", + "success": "Changed to scene %s.", + "exists_error": "The specified scene does not exist." + }, + "clear": { + "command_usage": "Usage: clear <all|wp|art|mat>", + "weapons": "Cleared weapons for %s.", + "artifacts": "Cleared artifacts for %s.", + "materials": "Cleared materials for %s.", + "furniture": "Cleared furniture for %s.", + "displays": "Cleared displays for %s.", + "virtuals": "Cleared virtuals for %s.", + "everything": "Cleared everything for %s." + }, + "coop": { + "usage": "Usage: coop <playerId> <target playerId>" + }, + "enter_dungeon": { + "usage": "Usage: enterdungeon <dungeon id>", + "changed": "Changed to dungeon %s", + "not_found_error": "Dungeon does not exist", + "in_dungeon_error": "You are already in that dungeon" + }, + "giveAll": { + "usage": "Usage: giveall [player] [amount]", + "item": "Giving all items...", + "done": "Giving all items done", + "invalid_amount_or_playerId": "Invalid amount or player ID." + }, + "giveArtifact": { + "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "invalid_artifact_id": "Invalid artifact ID.", + "given": "Given %s to %s." + }, + "giveChar": { + "usage": "Usage: givechar <player> <itemId|itemName> [amount]", + "given": "Given %s with level %s to %s.", + "invalid_avatar_id": "Invalid avatar id.", + "invalid_avatar_level": "Invalid avatar level.", + "invalid_avatar_or_player_id": "Invalid avatar or player ID." + }, + "give": { + "usage": "Usage: give <player> <itemId|itemName> [amount] [level]", + "refinement_only_applicable_weapons": "Refinement is only applicable to weapons.", + "refinement_must_between_1_and_5": "Refinement must be between 1 and 5.", + "given": "Given %s of %s to %s.", + "given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s", + "given_level": "Given %s with level %s %s times to %s" + }, + "godmode": { + "success": "Godmode is now %s for %s." + }, + "heal": { + "success": "All characters have been healed." + }, + "kick": { + "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", + "server_kick_player": "Kicking player [%s:%s]" + }, + "kill": { + "usage": "Usage: killall [playerUid] [sceneId]", + "scene_not_found_in_player_world": "Scene not found in player world", + "kill_monsters_in_scene": "Killing %s monsters in scene %s" + }, + "killCharacter": { + "usage": "Usage: /killcharacter [playerId]", + "current_character": "Killed %s current character." + }, + "list": { + "message": "There are %s player(s) online:" + }, + "permission": { + "usage": "Usage: permission <add|remove> <username> <permission>", + "add": "Permission added.", + "have_permission": "They already have this permission!", + "remove": "Permission removed.", + "not_have_permission": "They don't have this permission!" + }, + "position": { + "success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" + }, + "reload": { + "reload_start": "Reloading config.", + "reload_done": "Reload complete." + }, + "resetConst": { + "reset_all": "Reset all avatars' constellations.", + "success": "Constellations for %s have been reset. Please relog to see changes." + }, + "resetShopLimit": { + "usage": "Usage: /resetshop <player id>" + }, + "sendMail": { + "usage": "Usage: give [player] <itemId|itemName> [amount]", + "user_not_exist": "The user with an id of '%s' does not exist", + "start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time", + "templates": "Mail templates coming soon implemented...", + "invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`", + "send_cancel": "Message sending cancelled", + "send_done": "Message sent to user %s!", + "send_all_done": "Message sent to all users!", + "not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel", + "please_use": "Please use `/sendmail %s`", + "set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.", + "set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.", + "set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.", + "send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.", + "invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`", + "title": "<title>", + "message": "<message>", + "sender": "<sender>", + "arguments": "<itemId|itemName|finish> [amount] [level]", + "error": "ERROR: invalid construction stage %s. Check console for stacktrace." + }, + "sendMessage": { + "usage": "Usage: sendmessage <player> <message>", + "message_sent": "Message sent." + }, + "setFetterLevel": { + "usage": "Usage: setfetterlevel <level>", + "fetter_level_must_between_0_and_10": "Fetter level must be between 0 and 10.", + "fetter_set_level": "Fetter level set to %s", + "invalid_fetter_level": "Invalid fetter level." + }, + "setStats": { + "usage_console": "Usage: setstats|stats @<UID> <stat> <value>", + "usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>", + "help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n", + "value_error": "Invalid stat value.", + "uid_error": "Invalid UID.", + "player_error": "Player not found or offline.", + "set_self": "%s set to %s.", + "set_for_uid": "%s for %s set to %s.", + "set_max_hp": "MAX HP set to %s." + }, + "setWorldLevel": { + "usage": "Usage: setworldlevel <level>", + "world_level_must_between_0_and_8": "World level must be between 0-8", + "set_world_level": "World level set to %s.", + "invalid_world_level": "Invalid world level." + }, + "spawn": { + "usage": "Usage: spawn <entityId> [amount] [level(monster only)]", + "message": "Spawned %s of %s." + }, + "stop": { + "message": "Server shutting down..." + }, + "talent": { + "usage_1": "To set talent level: /talent set <talentID> <value>", + "usage_2": "Another way to set talent level: /talent <n or e or q> <value>", + "usage_3": "To get talent ID: /talent getid", + "lower_16": "Invalid talent level. Level should be lower than 16", + "set_id": "Set talent to %s.", + "set_atk": "Set talent Normal ATK to %s.", + "set_e": "Set talent E to %s.", + "set_q": "Set talent Q to %s.", + "invalid_skill_id": "Invalid skill ID.", + "set_this": "Set this talent to %s.", + "invalid_level": "Invalid talent level.", + "normal_attack_id": "Normal Attack ID %s.", + "e_skill_id": "E skill ID %s.", + "q_skill_id": "Q skill ID %s." + }, + "teleportAll": { + "message": "You only can use this command in MP mode." + }, + "teleport": { + "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", + "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", + "specify_player_id": "You must specify a player id.", + "invalid_position": "Invalid position.", + "message": "Teleported %s to %s,%s,%s in scene %s" + }, + "weather": { + "usage": "Usage: weather <weatherId> [climateId]", + "message": "Changed weather to %s with climate %s", + "invalid_id": "Invalid ID." + }, + "drop": { + "command_usage": "Usage: drop <itemId|itemName> [amount]", + "success": "Dropped %s of %s." + }, + "help": { + "usage": "Usage: ", + "aliases": "Aliases: ", + "available_commands": "Available commands: " + } + } +} \ No newline at end of file From d32d4014cd9fc09b17d83717b827800c0165494a Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Fri, 6 May 2022 14:10:23 +0800 Subject: [PATCH 117/434] Choose Avatar & Enter Tower --- proto/TowerBuffSelectReq.proto | 16 ++++ proto/TowerBuffSelectRsp.proto | 16 ++++ proto/TowerCurLevelRecordChangeNotify.proto | 17 +++++ proto/TowerEnterLevelReq.proto | 16 ++++ proto/TowerEnterLevelRsp.proto | 18 +++++ proto/TowerLevelStarCondData.proto | 9 +++ proto/TowerLevelStarCondNotify.proto | 19 +++++ proto/TowerTeamSelectReq.proto | 19 +++++ proto/TowerTeamSelectRsp.proto | 15 ++++ .../java/emu/grasscutter/data/GameData.java | 11 ++- .../grasscutter/data/def/TowerFloorData.java | 73 +++++++++++++++++++ .../grasscutter/data/def/TowerLevelData.java | 55 ++++++++++++++ .../game/dungeons/DungeonManager.java | 3 +- .../emu/grasscutter/game/player/Player.java | 11 +++ .../emu/grasscutter/game/player/TeamInfo.java | 5 ++ .../grasscutter/game/player/TeamManager.java | 66 ++++++++++++++--- .../grasscutter/game/tower/TowerManager.java | 40 ++++++++++ .../recv/HandlerTowerEnterLevelReq.java | 21 ++++++ .../recv/HandlerTowerTeamSelectReq.java | 26 +++++++ .../packet/send/PacketTowerAllDataRsp.java | 13 +++- .../packet/send/PacketTowerEnterLevelRsp.java | 22 ++++++ .../packet/send/PacketTowerTeamSelectRsp.java | 17 +++++ 22 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 proto/TowerBuffSelectReq.proto create mode 100644 proto/TowerBuffSelectRsp.proto create mode 100644 proto/TowerCurLevelRecordChangeNotify.proto create mode 100644 proto/TowerEnterLevelReq.proto create mode 100644 proto/TowerEnterLevelRsp.proto create mode 100644 proto/TowerLevelStarCondData.proto create mode 100644 proto/TowerLevelStarCondNotify.proto create mode 100644 proto/TowerTeamSelectReq.proto create mode 100644 proto/TowerTeamSelectRsp.proto create mode 100644 src/main/java/emu/grasscutter/data/def/TowerFloorData.java create mode 100644 src/main/java/emu/grasscutter/data/def/TowerLevelData.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java diff --git a/proto/TowerBuffSelectReq.proto b/proto/TowerBuffSelectReq.proto new file mode 100644 index 000000000..1641b350a --- /dev/null +++ b/proto/TowerBuffSelectReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerBuffSelectReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2424; + } + + uint32 tower_buff_id = 1; +} diff --git a/proto/TowerBuffSelectRsp.proto b/proto/TowerBuffSelectRsp.proto new file mode 100644 index 000000000..7a32a8e1e --- /dev/null +++ b/proto/TowerBuffSelectRsp.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerBuffSelectRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2491; + } + + int32 retcode = 1; + uint32 tower_buff_id = 2; +} diff --git a/proto/TowerCurLevelRecordChangeNotify.proto b/proto/TowerCurLevelRecordChangeNotify.proto new file mode 100644 index 000000000..fd9f94c97 --- /dev/null +++ b/proto/TowerCurLevelRecordChangeNotify.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerCurLevelRecord.proto"; + +message TowerCurLevelRecordChangeNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2489; + } + + TowerCurLevelRecord cur_level_record = 1; +} diff --git a/proto/TowerEnterLevelReq.proto b/proto/TowerEnterLevelReq.proto new file mode 100644 index 000000000..551f47729 --- /dev/null +++ b/proto/TowerEnterLevelReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerEnterLevelReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2412; + } + + uint32 enter_point_id = 1; +} diff --git a/proto/TowerEnterLevelRsp.proto b/proto/TowerEnterLevelRsp.proto new file mode 100644 index 000000000..fbcc4067f --- /dev/null +++ b/proto/TowerEnterLevelRsp.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerEnterLevelRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2426; + } + + int32 retcode = 1; + uint32 floor_id = 2; + uint32 level_index = 3; + repeated uint32 tower_buff_id_list = 4; +} diff --git a/proto/TowerLevelStarCondData.proto b/proto/TowerLevelStarCondData.proto new file mode 100644 index 000000000..d46334422 --- /dev/null +++ b/proto/TowerLevelStarCondData.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerLevelStarCondData { + uint32 star_cond_index = 3; + uint32 cond_value = 4; + bool is_pause = 5; +} diff --git a/proto/TowerLevelStarCondNotify.proto b/proto/TowerLevelStarCondNotify.proto new file mode 100644 index 000000000..e605496fb --- /dev/null +++ b/proto/TowerLevelStarCondNotify.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerLevelStarCondData.proto"; + +message TowerLevelStarCondNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2492; + } + + uint32 floor_id = 1; + uint32 level_index = 2; + repeated TowerLevelStarCondData cond_data_list = 3; +} diff --git a/proto/TowerTeamSelectReq.proto b/proto/TowerTeamSelectReq.proto new file mode 100644 index 000000000..903968061 --- /dev/null +++ b/proto/TowerTeamSelectReq.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerTeam.proto"; + +message TowerTeamSelectReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2401; + } + + uint32 floor_id = 1; + repeated TowerTeam tower_team_list = 2; +} diff --git a/proto/TowerTeamSelectRsp.proto b/proto/TowerTeamSelectRsp.proto new file mode 100644 index 000000000..b135e5364 --- /dev/null +++ b/proto/TowerTeamSelectRsp.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerTeamSelectRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2494; + } + + int32 retcode = 1; +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index e97e1e7de..c267a9b74 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -68,7 +68,9 @@ public class GameData { private static final Int2ObjectMap<ShopGoodsData> shopGoodsDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<CombineData> combineDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>(); - + private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap = new Int2ObjectOpenHashMap<>(); + // Cache private static Map<Integer, List<Integer>> fetters = new HashMap<>(); private static Map<Integer, List<ShopGoodsData>> shopGoods = new HashMap<>(); @@ -313,4 +315,11 @@ public class GameData { public static Int2ObjectMap<CombineData> getCombineDataMap() { return combineDataMap; } + + public static Int2ObjectMap<TowerFloorData> getTowerFloorDataMap(){ + return towerFloorDataMap; + } + public static Int2ObjectMap<TowerLevelData> getTowerLevelDataMap(){ + return towerLevelDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/def/TowerFloorData.java b/src/main/java/emu/grasscutter/data/def/TowerFloorData.java new file mode 100644 index 000000000..d9d0082c7 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerFloorData.java @@ -0,0 +1,73 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +@ResourceType(name = "TowerFloorExcelConfigData.json") +public class TowerFloorData extends GameResource { + + private int FloorId; + private int FloorIndex; + private int LevelId; + private int OverrideMonsterLevel; + private int TeamNum; + private int FloorLevelConfigId; + + @Override + public int getId() { + return this.FloorId; + } + + @Override + public void onLoad() { + super.onLoad(); + } + + public int getFloorId() { + return FloorId; + } + + public void setFloorId(int floorId) { + FloorId = floorId; + } + + public int getFloorIndex() { + return FloorIndex; + } + + public void setFloorIndex(int floorIndex) { + FloorIndex = floorIndex; + } + + public int getLevelId() { + return LevelId; + } + + public void setLevelId(int levelId) { + LevelId = levelId; + } + + public int getOverrideMonsterLevel() { + return OverrideMonsterLevel; + } + + public void setOverrideMonsterLevel(int overrideMonsterLevel) { + OverrideMonsterLevel = overrideMonsterLevel; + } + + public int getTeamNum() { + return TeamNum; + } + + public void setTeamNum(int teamNum) { + TeamNum = teamNum; + } + + public int getFloorLevelConfigId() { + return FloorLevelConfigId; + } + + public void setFloorLevelConfigId(int floorLevelConfigId) { + FloorLevelConfigId = floorLevelConfigId; + } +} diff --git a/src/main/java/emu/grasscutter/data/def/TowerLevelData.java b/src/main/java/emu/grasscutter/data/def/TowerLevelData.java new file mode 100644 index 000000000..6cc45cc06 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerLevelData.java @@ -0,0 +1,55 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +@ResourceType(name = "TowerLevelExcelConfigData.json") +public class TowerLevelData extends GameResource { + + private int ID; + private int LevelId; + private int LevelIndex; + private int DungeonId; + + @Override + public int getId() { + return this.ID; + } + + @Override + public void onLoad() { + super.onLoad(); + } + + public int getID() { + return ID; + } + + public void setID(int ID) { + this.ID = ID; + } + + public int getLevelId() { + return LevelId; + } + + public void setLevelId(int levelId) { + LevelId = levelId; + } + + public int getLevelIndex() { + return LevelIndex; + } + + public void setLevelIndex(int levelIndex) { + LevelIndex = levelIndex; + } + + public int getDungeonId() { + return DungeonId; + } + + public void setDungeonId(int dungeonId) { + DungeonId = dungeonId; + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index c84ef8a22..e858decf8 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -75,7 +75,8 @@ public class DungeonManager { prevPos.set(entry.getPointData().getTranPos()); } } - + // clean temp team if it has + player.getTeamManager().cleanTemporaryTeam(); // Transfer player back to world player.getWorld().transferPlayerToScene(player, prevScene, prevPos); player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp)); diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index c76f46bf0..9099c04c9 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -26,6 +26,7 @@ import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.managers.MapMarkManager.*; +import emu.grasscutter.game.tower.TowerManager; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; @@ -88,6 +89,8 @@ public class Player { @Transient private MessageHandler messageHandler; private TeamManager teamManager; + + private TowerManager towerManager; private PlayerGachaInfo gachaInfo; private PlayerProfile playerProfile; private boolean showAvatar; @@ -172,6 +175,7 @@ public class Player { this.nickname = "Traveler"; this.signature = ""; this.teamManager = new TeamManager(this); + this.towerManager = new TowerManager(this); this.birthday = new PlayerBirthday(); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); @@ -384,6 +388,10 @@ public class Player { return this.teamManager; } + public TowerManager getTowerManager() { + return towerManager; + } + public PlayerGachaInfo getGachaInfo() { return gachaInfo; } @@ -1020,6 +1028,9 @@ public class Player { if (this.getProfile().getUid() == 0) { this.getProfile().syncWithCharacter(this); } + if (this.getTowerManager() == null) { + this.towerManager = new TowerManager(this); + } // Check if player object exists in server // TODO - optimize diff --git a/src/main/java/emu/grasscutter/game/player/TeamInfo.java b/src/main/java/emu/grasscutter/game/player/TeamInfo.java index 5c66f1aaa..5794a7913 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamInfo.java +++ b/src/main/java/emu/grasscutter/game/player/TeamInfo.java @@ -18,6 +18,11 @@ public class TeamInfo { this.avatars = new ArrayList<>(Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); } + public TeamInfo(List<Integer> avatars) { + this.name = ""; + this.avatars = avatars; + } + public String getName() { return name; } diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 666c44dee..4e371b126 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -1,12 +1,6 @@ package emu.grasscutter.game.player; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Transient; @@ -58,7 +52,13 @@ public class TeamManager { @Transient private final Set<EntityBaseGadget> gadgets; @Transient private final IntSet teamResonances; @Transient private final IntSet teamResonancesConfig; - + + private int useTemporarilyTeamIndex = -1; + /** + * Temporary Team for tower + */ + private List<TeamInfo> temporaryTeam; + public TeamManager() { this.mpTeam = new TeamInfo(); this.avatars = new ArrayList<>(); @@ -124,6 +124,10 @@ public class TeamManager { } public TeamInfo getCurrentTeamInfo() { + if (useTemporarilyTeamIndex >= 0 && + useTemporarilyTeamIndex < temporaryTeam.size()){ + return temporaryTeam.get(useTemporarilyTeamIndex); + } if (this.getPlayer().isInMultiplayer()) { return this.getMpTeam(); } @@ -351,7 +355,51 @@ public class TeamManager { // Packet this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(getPlayer(), teamInfo)); } - + + public void setupTemporaryTeam(List<List<Long>> guidList) { + var team = guidList.stream().map(list -> { + // Sanity checks + if (list.size() == 0 || list.size() > getMaxTeamSize()) { + return null; + } + + // Set team data + LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); + for (int i = 0; i < list.size(); i++) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return null; + } + newTeam.add(avatar); + } + + // convert to avatar ids + return newTeam.stream() + .map(Avatar::getAvatarId) + .toList(); + }) + .filter(Objects::nonNull) + .map(TeamInfo::new) + .toList(); + this.temporaryTeam = team; + } + + public void useTemporaryTeam(int index) { + this.useTemporarilyTeamIndex = index; + updateTeamEntities(null); + } + + public void cleanTemporaryTeam() { + // check if using temporary team + if(useTemporarilyTeamIndex < 0){ + return; + } + + this.useTemporarilyTeamIndex = -1; + this.temporaryTeam = null; + updateTeamEntities(null); + } public synchronized void setCurrentTeam(int teamId) { // if (getPlayer().isInMultiplayer()) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java new file mode 100644 index 000000000..e49a15cc2 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -0,0 +1,40 @@ +package emu.grasscutter.game.tower; + +import dev.morphia.annotations.Entity; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; + +import java.util.List; + +@Entity +public class TowerManager { + private final Player player; + + public TowerManager(Player player) { + this.player = player; + } + private int currentLevel; + private int currentFloor; + + public void teamSelect(int floor, List<List<Long>> towerTeams) { + var floorData = GameData.getTowerFloorDataMap().get(floor); + + this.currentFloor = floorData.getFloorId(); + this.currentLevel = floorData.getLevelId(); + + player.getTeamManager().setupTemporaryTeam(towerTeams); + } + + + public void enterLevel(int enterPointId) { + var levelData = GameData.getTowerLevelDataMap().get(currentLevel); + var id = levelData.getDungeonId(); + // use team user choose + player.getTeamManager().useTemporaryTeam(0); + player.getServer().getDungeonManager() + .enterDungeon(player, enterPointId, id); + + player.getSession().send(new PacketTowerEnterLevelRsp(currentFloor, currentLevel)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java new file mode 100644 index 000000000..163f101ed --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerEnterLevelReqOuterClass.TowerEnterLevelReq; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.TowerEnterLevelReq) +public class HandlerTowerEnterLevelReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + TowerEnterLevelReq req = TowerEnterLevelReq.parseFrom(payload); + + //session.send(new PacketTowerCurLevelRecordChangeNotify()); + session.getPlayer().getTowerManager().enterLevel(req.getEnterPointId()); + + //session.send(new PacketTowerLevelStarCondNotify()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java new file mode 100644 index 000000000..6e6705379 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerTeamOuterClass; +import emu.grasscutter.net.proto.TowerTeamSelectReqOuterClass.TowerTeamSelectReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketTowerTeamSelectRsp; + +@Opcodes(PacketOpcodes.TowerTeamSelectReq) +public class HandlerTowerTeamSelectReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + TowerTeamSelectReq req = TowerTeamSelectReq.parseFrom(payload); + + var towerTeams = req.getTowerTeamListList().stream() + .map(TowerTeamOuterClass.TowerTeam::getAvatarGuidListList) + .toList(); + + session.getPlayer().getTowerManager().teamSelect(req.getFloorId(), towerTeams); + + session.send(new PacketTowerTeamSelectRsp()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java index 2bd1d0171..d2d2376e6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java @@ -1,19 +1,28 @@ package emu.grasscutter.server.packet.send; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.TowerFloorData; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.TowerAllDataRspOuterClass.TowerAllDataRsp; import emu.grasscutter.net.proto.TowerCurLevelRecordOuterClass.TowerCurLevelRecord; import emu.grasscutter.net.proto.TowerFloorRecordOuterClass.TowerFloorRecord; +import java.util.stream.Collectors; + public class PacketTowerAllDataRsp extends BasePacket { public PacketTowerAllDataRsp() { super(PacketOpcodes.TowerAllDataRsp); - + + var list = GameData.getTowerFloorDataMap().values().stream() + .map(TowerFloorData::getFloorId) + .map(id -> TowerFloorRecord.newBuilder().setFloorId(id).build()) + .collect(Collectors.toList()); + TowerAllDataRsp proto = TowerAllDataRsp.newBuilder() .setTowerScheduleId(29) - .addTowerFloorRecordList(TowerFloorRecord.newBuilder().setFloorId(1001)) + .addAllTowerFloorRecordList(list) .setCurLevelRecord(TowerCurLevelRecord.newBuilder().setIsEmpty(true)) .setNextScheduleChangeTime(Integer.MAX_VALUE) .putFloorOpenTimeMap(1024, 1630486800) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java new file mode 100644 index 000000000..ebb8fb2b2 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerEnterLevelRspOuterClass.TowerEnterLevelRsp; + +public class PacketTowerEnterLevelRsp extends BasePacket { + + public PacketTowerEnterLevelRsp(int floorId, int levelIndex) { + super(PacketOpcodes.TowerEnterLevelRsp); + + TowerEnterLevelRsp proto = TowerEnterLevelRsp.newBuilder() + .setFloorId(floorId) + .setLevelIndex(levelIndex) + .addTowerBuffIdList(4) + .addTowerBuffIdList(28) + .addTowerBuffIdList(18) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java new file mode 100644 index 000000000..445b707cd --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerTeamSelectRspOuterClass.TowerTeamSelectRsp; + +public class PacketTowerTeamSelectRsp extends BasePacket { + + public PacketTowerTeamSelectRsp() { + super(PacketOpcodes.TowerTeamSelectRsp); + + TowerTeamSelectRsp proto = TowerTeamSelectRsp.newBuilder() + .build(); + + this.setData(proto); + } +} From 744aa478a979c3ec21b1b6f715eccb618de3fb15 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Thu, 5 May 2022 22:07:29 -0700 Subject: [PATCH 118/434] Add drowning. Better movement ticking. --- proto/WorldPlayerReviveReq.proto | 15 + .../grasscutter/game/entity/EntityAvatar.java | 5 + .../managers/MotionManager/MotionManager.java | 227 ----------- .../MovementManager/MovementManager.java | 359 ++++++++++++++++++ .../emu/grasscutter/game/player/Player.java | 34 +- .../grasscutter/game/player/TeamManager.java | 63 +-- .../recv/HandlerCombatInvocationsNotify.java | 15 +- .../recv/HandlerWorldPlayerReviveReq.java | 3 + .../PacketAvatarLifeStateChangeNotify.java | 28 +- .../send/PacketLifeStateChangeNotify.java | 29 +- .../send/PacketWorldPlayerReviveRsp.java | 18 + 11 files changed, 500 insertions(+), 296 deletions(-) create mode 100644 proto/WorldPlayerReviveReq.proto delete mode 100644 src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java create mode 100644 src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerReviveRsp.java diff --git a/proto/WorldPlayerReviveReq.proto b/proto/WorldPlayerReviveReq.proto new file mode 100644 index 000000000..edaeab683 --- /dev/null +++ b/proto/WorldPlayerReviveReq.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +option csharp_namespace = "YSFreedom.Common.Protocol"; + + +message WorldPlayerReviveReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 288; + } + +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index a21e14360..82efb795f 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -104,6 +104,11 @@ public class EntityAvatar extends GameEntity { this.killedType = PlayerDieType.PLAYER_DIE_KILL_BY_MONSTER; this.killedBy = killerId; } + + public void onDeath(PlayerDieType dieType, int killerId) { + this.killedType = dieType; + this.killedBy = killerId; + } public SceneAvatarInfo getSceneAvatarInfo() { SceneAvatarInfo.Builder avatarInfo = SceneAvatarInfo.newBuilder() diff --git a/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java b/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java deleted file mode 100644 index d8d6f25b0..000000000 --- a/src/main/java/emu/grasscutter/game/managers/MotionManager/MotionManager.java +++ /dev/null @@ -1,227 +0,0 @@ -package emu.grasscutter.game.managers.MotionManager; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.VectorOuterClass; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; -import emu.grasscutter.server.packet.send.PacketPlayerPropNotify; -import emu.grasscutter.utils.Position; - -import java.util.ArrayList; -import java.lang.Math; - -public class MotionManager { - - private enum Consumption { - None(0), - - // consumers - CLIMB_START(-500), - CLIMBING(-150), - CLIMB_JUMP(-2500), - DASH(-1800), - SPRINT(-360), - FLY(-60), - SWIM_DASH_START(-200), - SWIM_DASH(-200), - SWIMMING(-80), - - // restorers - STANDBY(500), - RUN(500), - WALK(500), - STANDBY_MOVE(500); - - public final int amount; - Consumption(int amount) { - this.amount = amount; - } - } - - private EntityMoveInfoOuterClass.EntityMoveInfo moveInfo; - - private MotionState previousState = MotionState.MOTION_STANDBY; - private ArrayList<Position> previousCoordinates = new ArrayList<>(); - private final Player player; - - private float landSpeed = 0; - - public MotionManager(Player player) { - previousCoordinates.add(new Position(0,0,0)); - this.player = player; - } - - public void handle(GameSession session, GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { - MotionState state = moveInfo.getMotionInfo().getState(); - setMoveInfo(moveInfo); - if (state == MotionState.MOTION_LAND_SPEED) { - setLandSpeed(moveInfo.getMotionInfo().getSpeed().getY()); - } - if (state == MotionState.MOTION_FALL_ON_GROUND) { - handleFallOnGround(session, entity); - } - } - - public void tick() { - if(Grasscutter.getConfig().OpenStamina){ - EntityMoveInfoOuterClass.EntityMoveInfo mInfo = moveInfo; - if (mInfo == null) { - return; - } - - MotionState state = moveInfo.getMotionInfo().getState(); - Consumption consumption = Consumption.None; - - boolean isMoving = false; - VectorOuterClass.Vector posVector = moveInfo.getMotionInfo().getPos(); - Position currentCoordinates = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); - - float diffX = currentCoordinates.getX() - previousCoordinates.get(0).getX(); - float diffY = currentCoordinates.getY() - previousCoordinates.get(0).getY(); - float diffZ = currentCoordinates.getZ() - previousCoordinates.get(0).getZ(); - - if (Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.3 || Math.abs(diffZ) > 0.3) { - isMoving = true; - } - - if (isMoving) { - // TODO: refactor these conditions. - // CLIMB - if (state == MotionState.MOTION_CLIMB) { - if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = Consumption.CLIMB_START; - } else { - consumption = Consumption.CLIMBING; - } - } - // JUMP - if (state == MotionState.MOTION_CLIMB_JUMP) { - if (previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = Consumption.CLIMB_JUMP; - } - } - if (state == MotionState.MOTION_JUMP) { - if (previousState == MotionState.MOTION_CLIMB) { - consumption = Consumption.CLIMB_JUMP; - } - } - // SWIM - if (state == MotionState.MOTION_SWIM_MOVE) { - consumption = Consumption.SWIMMING; - } - if (state == MotionState.MOTION_SWIM_DASH) { - if (previousState != MotionState.MOTION_SWIM_DASH) { - consumption = Consumption.SWIM_DASH_START; - } else { - consumption = Consumption.SWIM_DASH; - } - } - // DASH - if (state == MotionState.MOTION_DASH) { - if (previousState == MotionState.MOTION_DASH) { - consumption = Consumption.SPRINT; - } else { - consumption = Consumption.DASH; - } - } - // RUN and WALK - if (state == MotionState.MOTION_RUN) { - consumption = Consumption.RUN; - } - if (state == MotionState.MOTION_WALK) { - consumption = Consumption.WALK; - } - // FLY - if (state == MotionState.MOTION_FLY) { - consumption = Consumption.FLY; - } - } - // STAND - if (state == MotionState.MOTION_STANDBY) { - consumption = Consumption.STANDBY; - } - if (state == MotionState.MOTION_STANDBY_MOVE) { - consumption = Consumption.STANDBY_MOVE; - } - - GameSession session = player.getSession(); - updateStamina(session, consumption.amount); - session.send(new PacketPlayerPropNotify(session.getPlayer(), PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - - Grasscutter.getLogger().debug(session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + " " + state + " " + isMoving + " " + consumption + " " + consumption.amount); - - previousState = state; - previousCoordinates.add(currentCoordinates); - if (previousCoordinates.size() > 3) { - previousCoordinates.remove(0); - } - } - } - - public void updateStamina(GameSession session, int amount) { - if (amount == 0) { - return; - } - int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); - int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); - int newStamina = currentStamina + amount; - if (newStamina < 0) { - newStamina = 0; - } - if (newStamina > playerMaxStamina) { - newStamina = playerMaxStamina; - } - session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); - } - - public void setMoveInfo(EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { - this.moveInfo = moveInfo; - } - - public EntityMoveInfoOuterClass.EntityMoveInfo getMoveInfo() { - return moveInfo; - } - - public void handleFallOnGround(GameSession session, GameEntity entity) { - float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float damage = 0; - Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); - if (landSpeed < -23.5) { - damage = (float)(maxHP * 0.33); - } - if (landSpeed < -25) { - damage = (float)(maxHP * 0.5); - } - if (landSpeed < -26.5) { - damage = (float)(maxHP * 0.66); - } - if (landSpeed < -28) { - damage = (maxHP * 1); - } - float newHP = currentHP - damage; - if (newHP < 0) { - newHP = 0; - } - Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); - entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); - if (newHP == 0) { - entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); - session.getPlayer().getScene().removeEntity(entity); - entity.onDeath(0); - } - } - - public void setLandSpeed(float landSpeed) { - this.landSpeed = landSpeed; - } -} diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java new file mode 100644 index 000000000..a08280378 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -0,0 +1,359 @@ +package emu.grasscutter.game.managers.MovementManager; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.LifeState; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.VectorOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.Position; +import org.jetbrains.annotations.NotNull; + +import java.lang.Math; +import java.util.*; + +public class MovementManager { + + public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); + + private enum Consumption { + None(0), + + // consume + CLIMB_START(-500), + CLIMBING(-150), + CLIMB_JUMP(-2500), + DASH(-1800), + SPRINT(-360), + FLY(-60), + SWIM_DASH_START(-200), + SWIM_DASH(-200), + SWIMMING(-80), + + // restore + STANDBY(500), + RUN(500), + WALK(500), + STANDBY_MOVE(500), + POWERED_FLY(500); + + public final int amount; + Consumption(int amount) { + this.amount = amount; + } + } + + + private MotionState previousState = MotionState.MOTION_STANDBY; + private MotionState currentState = MotionState.MOTION_STANDBY; + private Position previousCoordinates = new Position(0, 0, 0); + private Position currentCoordinates = new Position(0, 0, 0); + + private final Player player; + + private float landSpeed = 0; + private Timer movementManagerTickTimer; + private GameSession cachedSession = null; + private GameEntity cachedEntity = null; + + private int staminaRecoverDelay = 0; + + public MovementManager(Player player) { + previousCoordinates.add(new Position(0,0,0)); + this.player = player; + + MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( + MotionState.MOTION_SWIM_MOVE, + MotionState.MOTION_SWIM_IDLE, + MotionState.MOTION_SWIM_DASH, + MotionState.MOTION_SWIM_JUMP + ))); + + MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList( + MotionState.MOTION_STANDBY, + MotionState.MOTION_STANDBY_MOVE, + MotionState.MOTION_DANGER_STANDBY, + MotionState.MOTION_DANGER_STANDBY_MOVE, + MotionState.MOTION_LADDER_TO_STANDBY, + MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY + ))); + + MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList( + MotionState.MOTION_CLIMB, + MotionState.MOTION_CLIMB_JUMP, + MotionState.MOTION_STANDBY_TO_CLIMB, + MotionState.MOTION_LADDER_IDLE, + MotionState.MOTION_LADDER_MOVE, + MotionState.MOTION_LADDER_SLIP, + MotionState.MOTION_STANDBY_TO_LADDER + ))); + + MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList( + MotionState.MOTION_FLY, + MotionState.MOTION_FLY_IDLE, + MotionState.MOTION_FLY_SLOW, + MotionState.MOTION_FLY_FAST, + MotionState.MOTION_POWERED_FLY + ))); + + MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList( + MotionState.MOTION_DASH, + MotionState.MOTION_DANGER_DASH, + MotionState.MOTION_DASH_BEFORE_SHAKE, + MotionState.MOTION_RUN, + MotionState.MOTION_DANGER_RUN, + MotionState.MOTION_WALK, + MotionState.MOTION_DANGER_WALK + ))); + } + + public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) { + if (movementManagerTickTimer == null) { + movementManagerTickTimer = new Timer(); + movementManagerTickTimer.scheduleAtFixedRate(new MotionManagerTick(), 0, 200); + } + // cache info for later use in tick + cachedSession = session; + cachedEntity = entity; + + MotionInfo motionInfo = moveInfo.getMotionInfo(); + moveEntity(entity, moveInfo); + VectorOuterClass.Vector posVector = motionInfo.getPos(); + Position newPos = new Position(posVector.getX(), + posVector.getY(), posVector.getZ());; + if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { + currentCoordinates = newPos; + } + currentState = motionInfo.getState(); + Grasscutter.getLogger().debug("" + currentState); + handleFallOnGround(motionInfo); + } + + public void resetTimer() { + movementManagerTickTimer.cancel(); + movementManagerTickTimer = null; + } + + private void moveEntity(GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { + entity.getPosition().set(moveInfo.getMotionInfo().getPos()); + entity.getRotation().set(moveInfo.getMotionInfo().getRot()); + entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); + entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); + entity.setMotionState(moveInfo.getMotionInfo().getState()); + } + + private boolean isPlayerMoving() { + float diffX = currentCoordinates.getX() - previousCoordinates.getX(); + float diffY = currentCoordinates.getY() - previousCoordinates.getY(); + float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); + // Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); + return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2; + } + + private int getCurrentStamina() { + return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + } + + private int getMaximumStamina() { + return player.getProperty(PlayerProperty.PROP_MAX_STAMINA); + } + + + + // Returns new stamina + public int updateStamina(GameSession session, int amount) { + int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + if (amount == 0) { + return currentStamina; + } + int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); + int newStamina = currentStamina + amount; + if (newStamina < 0) { + newStamina = 0; + } + if (newStamina > playerMaxStamina) { + newStamina = playerMaxStamina; + } + session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + return newStamina; + } + + private void handleFallOnGround(@NotNull MotionInfo motionInfo) { + MotionState state = motionInfo.getState(); + // land speed and fall on ground event arrive in different packets + // cache land speed + if (state == MotionState.MOTION_LAND_SPEED) { + landSpeed = motionInfo.getSpeed().getY(); + } + if (state == MotionState.MOTION_FALL_ON_GROUND) { + float currentHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float damage = 0; +// Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); + if (landSpeed < -23.5) { + damage = (float)(maxHP * 0.33); + } + if (landSpeed < -25) { + damage = (float)(maxHP * 0.5); + } + if (landSpeed < -26.5) { + damage = (float)(maxHP * 0.66); + } + if (landSpeed < -28) { + damage = (maxHP * 1); + } + float newHP = currentHP - damage; + if (newHP < 0) { + newHP = 0; + } +// Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); + cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); + if (newHP == 0) { + killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_FALL); + } + landSpeed = 0; + } + } + + private void handleDrowning() { + int stamina = getCurrentStamina(); + if (stamina < 10) { + boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState); + Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming); + if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) { + killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); + } + } + } + + public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) { + cachedSession.send(new PacketAvatarLifeStateChangeNotify( + cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), + LifeState.LIFE_DEAD, + dieType + )); + cachedSession.send(new PacketLifeStateChangeNotify( + cachedEntity, + LifeState.LIFE_DEAD, + dieType + )); + cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0); + cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); + entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); + session.getPlayer().getScene().removeEntity(entity); + ((EntityAvatar)entity).onDeath(dieType, 0); + } + + private class MotionManagerTick extends TimerTask + { + public void run() { + if (Grasscutter.getConfig().OpenStamina) { + boolean moving = isPlayerMoving(); + if (moving || (getCurrentStamina() < getMaximumStamina())) { + Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); + Consumption consumption = Consumption.None; + + // TODO: refactor these conditions. + if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { + if (currentState == MotionState.MOTION_CLIMB) { + // CLIMB + if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = Consumption.CLIMB_START; + } else { + consumption = Consumption.CLIMBING; + } + } + if (currentState == MotionState.MOTION_CLIMB_JUMP) { + if (previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = Consumption.CLIMB_JUMP; + } + } + if (currentState == MotionState.MOTION_JUMP) { + if (previousState == MotionState.MOTION_CLIMB) { + consumption = Consumption.CLIMB_JUMP; + } + } + } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { + // SWIM + if (currentState == MotionState.MOTION_SWIM_MOVE) { + consumption = Consumption.SWIMMING; + } + if (currentState == MotionState.MOTION_SWIM_DASH) { + if (previousState != MotionState.MOTION_SWIM_DASH) { + consumption = Consumption.SWIM_DASH_START; + } else { + consumption = Consumption.SWIM_DASH; + } + } + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + // RUN, DASH and WALK + // DASH + if (currentState == MotionState.MOTION_DASH) { + if (previousState == MotionState.MOTION_DASH) { + consumption = Consumption.SPRINT; + } else { + // cost more to start dashing + consumption = Consumption.DASH; + } + } + // RUN + if (currentState == MotionState.MOTION_RUN) { + consumption = Consumption.RUN; + } + // WALK + if (currentState == MotionState.MOTION_WALK) { + consumption = Consumption.WALK; + } + } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { + // FLY + consumption = Consumption.FLY; + // POWERED_FLY, e.g. wind tunnel + if (currentState == MotionState.MOTION_POWERED_FLY) { + consumption = Consumption.POWERED_FLY; + } + } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { + // STAND + if (currentState == MotionState.MOTION_STANDBY) { + consumption = Consumption.STANDBY; + } + if (currentState == MotionState.MOTION_STANDBY_MOVE) { + consumption = Consumption.STANDBY_MOVE; + } + } + + // tick triggered + handleDrowning(); + + if (cachedSession != null) { + if (consumption.amount < 0) { + staminaRecoverDelay = 0; + } + if (consumption.amount > 0) { + if (staminaRecoverDelay < 5) { + staminaRecoverDelay++; + consumption = Consumption.None; + } + } + int newStamina = updateStamina(cachedSession, consumption.amount); + cachedSession.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); + + Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t" + consumption + "(" + consumption.amount + ")"); + } + } + } + + previousState = currentState; + previousCoordinates = new Position(currentCoordinates.getX(), + currentCoordinates.getY(), currentCoordinates.getZ());; + } + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index b1abda002..f6ea68dc6 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -21,7 +21,7 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.MailHandler; -import emu.grasscutter.game.managers.MotionManager.MotionManager; +import emu.grasscutter.game.managers.MovementManager.MovementManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; @@ -122,7 +122,7 @@ public class Player { @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; private MapMarksManager mapMarksManager; - @Transient private MotionManager motionManager; + @Transient private MovementManager movementManager; @Deprecated @@ -164,7 +164,7 @@ public class Player { this.shopLimit = new ArrayList<>(); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); - this.motionManager = new MotionManager(this); + this.movementManager = new MovementManager(this); } // On player creation @@ -191,7 +191,7 @@ public class Player { this.getRotation().set(0, 307, 0); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); - this.motionManager = new MotionManager(this); + this.movementManager = new MovementManager(this); } public int getUid() { @@ -974,7 +974,7 @@ public class Player { return mapMarksManager; } - public MotionManager getMotionManager() { return motionManager; } + public MovementManager getMovementManager() { return movementManager; } public synchronized void onTick() { // Check ping @@ -1004,33 +1004,9 @@ public class Player { this.resetSendPlayerLocTime(); } } - - scheduleStaminaNotify(); } - private void scheduleStaminaNotify() { - // stamina tick - EntityMoveInfoOuterClass.EntityMoveInfo moveInfo = getMotionManager().getMoveInfo(); - if (moveInfo == null) { - return; - } - if (getMotionManager().getMoveInfo().getMotionInfo().getState() == MotionStateOuterClass.MotionState.MOTION_STANDBY) { - if (getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) == getProperty(PlayerProperty.PROP_MAX_STAMINA) ) { - return; - } - } - - for (int i = 0; i <= 1000; i+=200) { - Timer timer = new Timer(); - timer.schedule(new TimerTask() { - @Override - public void run() { - getMotionManager().tick(); - } - }, i); - } - } public void resetSendPlayerLocTime() { diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 666c44dee..ff9a3c68d 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -24,6 +24,7 @@ import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; import emu.grasscutter.server.packet.send.PacketAvatarDieAnimationEndRsp; import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify; @@ -419,27 +420,37 @@ public class TeamManager { if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) { return; } - - // Replacement avatar - EntityAvatar replacement = null; - int replaceIndex = -1; - - for (int i = 0; i < this.getActiveTeam().size(); i++) { - EntityAvatar entity = this.getActiveTeam().get(i); - if (entity.isAlive()) { - replaceIndex = i; - replacement = entity; - break; - } - } - - if (replacement == null) { - // No more living team members... - getPlayer().sendPacket(new PacketWorldPlayerDieNotify(deadAvatar.getKilledType(), deadAvatar.getKilledBy())); + + PlayerDieType dieType = deadAvatar.getKilledType(); + int killedBy = deadAvatar.getKilledBy(); + + if (dieType == PlayerDieType.PLAYER_DIE_DRAWN) { + // Died in water. Do not replace + // The official server has skipped this notify and will just respawn the team immediately after the animation. + // TODO: Perhaps find a way to get vanilla experience? + getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); } else { - // Set index and spawn replacement member - this.setCurrentCharacterIndex(replaceIndex); - getPlayer().getScene().addEntity(replacement); + // Replacement avatar + EntityAvatar replacement = null; + int replaceIndex = -1; + + for (int i = 0; i < this.getActiveTeam().size(); i++) { + EntityAvatar entity = this.getActiveTeam().get(i); + if (entity.isAlive()) { + replaceIndex = i; + replacement = entity; + break; + } + } + + if (replacement == null) { + // No more living team members... + getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); + } else { + // Set index and spawn replacement member + this.setCurrentCharacterIndex(replaceIndex); + getPlayer().getScene().addEntity(replacement); + } } // Response packet @@ -492,11 +503,13 @@ public class TeamManager { public void respawnTeam() { // Make sure all team members are dead - for (EntityAvatar entity : getActiveTeam()) { - if (entity.isAlive()) { - return; - } - } + // Drowning needs revive when there may be other team members still alive. + // for (EntityAvatar entity : getActiveTeam()) { + // if (entity.isAlive()) { + // return; + // } + // } + player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn // Revive all team members for (EntityAvatar entity : getActiveTeam()) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 878997e22..27e4ca6ff 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -1,9 +1,6 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.managers.MotionManager.MotionManager; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; @@ -11,9 +8,7 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.*; @Opcodes(PacketOpcodes.CombatInvocationsNotify) public class HandlerCombatInvocationsNotify extends PacketHandler { @@ -33,13 +28,7 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); if (entity != null) { - //move - entity.getPosition().set(moveInfo.getMotionInfo().getPos()); - entity.getRotation().set(moveInfo.getMotionInfo().getRot()); - entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); - entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); - entity.setMotionState(moveInfo.getMotionInfo().getState()); - session.getPlayer().getMotionManager().handle(session, entity, moveInfo); + session.getPlayer().getMovementManager().handle(session, moveInfo, entity); } break; default: @@ -52,7 +41,7 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { if (notif.getInvokeListList().size() > 0) { session.getPlayer().getCombatInvokeHandler().update(session.getPlayer()); } - // Handle attack results last + // Handle attack results last while (!session.getPlayer().getAttackResults().isEmpty()) { session.getPlayer().getScene().handleAttack(session.getPlayer().getAttackResults().poll()); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerWorldPlayerReviveReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerWorldPlayerReviveReq.java index cbe95a322..6872dd270 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerWorldPlayerReviveReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerWorldPlayerReviveReq.java @@ -3,7 +3,9 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.proto.WorldPlayerDieNotifyOuterClass; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketWorldPlayerReviveRsp; @Opcodes(PacketOpcodes.WorldPlayerReviveReq) public class HandlerWorldPlayerReviveReq extends PacketHandler { @@ -11,6 +13,7 @@ public class HandlerWorldPlayerReviveReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { session.getPlayer().getTeamManager().respawnTeam(); + session.send(new PacketWorldPlayerReviveRsp()); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarLifeStateChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarLifeStateChangeNotify.java index 3032bf355..d7258f6fa 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarLifeStateChangeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarLifeStateChangeNotify.java @@ -9,6 +9,10 @@ import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.AvatarLifeStateChangeNotifyOuterClass.AvatarLifeStateChangeNotify; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.ServerBuffOuterClass; +import emu.grasscutter.net.proto.ServerBuffOuterClass.ServerBuff; + +import java.util.ArrayList; public class PacketAvatarLifeStateChangeNotify extends BasePacket { @@ -22,7 +26,7 @@ public class PacketAvatarLifeStateChangeNotify extends BasePacket { this.setData(proto); } - public PacketAvatarLifeStateChangeNotify(Avatar avatar,int attackerId,LifeState lifeState) { + public PacketAvatarLifeStateChangeNotify(Avatar avatar, int attackerId, LifeState lifeState) { super(PacketOpcodes.AvatarLifeStateChangeNotify); AvatarLifeStateChangeNotify proto = AvatarLifeStateChangeNotify.newBuilder() @@ -33,4 +37,26 @@ public class PacketAvatarLifeStateChangeNotify extends BasePacket { this.setData(proto); } + + public PacketAvatarLifeStateChangeNotify(Avatar avatar, LifeState lifeState, PlayerDieType dieType) { + this(avatar, lifeState, null, "", dieType); + } + + public PacketAvatarLifeStateChangeNotify(Avatar avatar, LifeState lifeState, GameEntity sourceEntity, + String attackTag, PlayerDieType dieType) { + super(PacketOpcodes.AvatarLifeStateChangeNotify); + + AvatarLifeStateChangeNotify.Builder proto = AvatarLifeStateChangeNotify.newBuilder(); + + proto.setAvatarGuid(avatar.getGuid()); + proto.setLifeState(lifeState.getValue()); + if (sourceEntity != null) { + proto.setSourceEntityId(sourceEntity.getId()); + } + proto.setDieType(dieType); + proto.setAttackTag((attackTag)); + + this.setData(proto.build()); + } + } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketLifeStateChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketLifeStateChangeNotify.java index c70d117d7..75685a27e 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketLifeStateChangeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketLifeStateChangeNotify.java @@ -1,10 +1,15 @@ package emu.grasscutter.server.packet.send; +import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.props.LifeState; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.LifeStateChangeNotifyOuterClass.LifeStateChangeNotify; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.ServerBuffOuterClass.ServerBuff; + +import java.util.ArrayList; public class PacketLifeStateChangeNotify extends BasePacket { public PacketLifeStateChangeNotify(GameEntity attacker, GameEntity target, LifeState lifeState) { @@ -26,7 +31,29 @@ public class PacketLifeStateChangeNotify extends BasePacket { .setLifeState(lifeState.getValue()) .setSourceEntityId(attackerId) .build(); - + this.setData(proto); } + + public PacketLifeStateChangeNotify(GameEntity entity, LifeState lifeState, PlayerDieType dieType) { + this(entity, lifeState, null, "", dieType); + } + + public PacketLifeStateChangeNotify(GameEntity entity, LifeState lifeState, GameEntity sourceEntity, + String attackTag, PlayerDieType dieType) { + super(PacketOpcodes.LifeStateChangeNotify); + + LifeStateChangeNotify.Builder proto = LifeStateChangeNotify.newBuilder(); + + + proto.setEntityId(entity.getId()); + proto.setLifeState(lifeState.getValue()); + if (sourceEntity != null) { + proto.setSourceEntityId(sourceEntity.getId()); + } + proto.setAttackTag(attackTag); + proto.setDieType(dieType); + + this.setData(proto.build()); + } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerReviveRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerReviveRsp.java new file mode 100644 index 000000000..ff93d2b00 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldPlayerReviveRsp.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.world.World; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.WorldPlayerReviveRspOuterClass.WorldPlayerReviveRsp; + +public class PacketWorldPlayerReviveRsp extends BasePacket { + + public PacketWorldPlayerReviveRsp() { + super(PacketOpcodes.WorldPlayerReviveRsp); + + WorldPlayerReviveRsp.Builder proto = WorldPlayerReviveRsp.newBuilder(); + + this.setData(proto.build()); + } +} From a79e00896cb99b783cb0bcef48a431a26da4cc24 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Fri, 6 May 2022 14:46:10 +0800 Subject: [PATCH 119/434] Add @Transient for temporary team --- src/main/java/emu/grasscutter/game/player/TeamManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 4e371b126..8b8a10355 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -53,11 +53,11 @@ public class TeamManager { @Transient private final IntSet teamResonances; @Transient private final IntSet teamResonancesConfig; - private int useTemporarilyTeamIndex = -1; + @Transient private int useTemporarilyTeamIndex = -1; /** * Temporary Team for tower */ - private List<TeamInfo> temporaryTeam; + @Transient private List<TeamInfo> temporaryTeam; public TeamManager() { this.mpTeam = new TeamInfo(); From 696f6290804a81e7aa434f4c410df8d97222c58a Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Fri, 6 May 2022 14:10:23 +0800 Subject: [PATCH 120/434] Choose Avatar & Enter Tower --- proto/TowerBuffSelectReq.proto | 16 ++++ proto/TowerBuffSelectRsp.proto | 16 ++++ proto/TowerCurLevelRecordChangeNotify.proto | 17 +++++ proto/TowerEnterLevelReq.proto | 16 ++++ proto/TowerEnterLevelRsp.proto | 18 +++++ proto/TowerLevelStarCondData.proto | 9 +++ proto/TowerLevelStarCondNotify.proto | 19 +++++ proto/TowerTeamSelectReq.proto | 19 +++++ proto/TowerTeamSelectRsp.proto | 15 ++++ .../java/emu/grasscutter/data/GameData.java | 11 ++- .../grasscutter/data/def/TowerFloorData.java | 73 +++++++++++++++++++ .../grasscutter/data/def/TowerLevelData.java | 55 ++++++++++++++ .../game/dungeons/DungeonManager.java | 3 +- .../emu/grasscutter/game/player/Player.java | 11 +++ .../emu/grasscutter/game/player/TeamInfo.java | 5 ++ .../grasscutter/game/player/TeamManager.java | 66 ++++++++++++++--- .../grasscutter/game/tower/TowerManager.java | 40 ++++++++++ .../recv/HandlerTowerEnterLevelReq.java | 21 ++++++ .../recv/HandlerTowerTeamSelectReq.java | 26 +++++++ .../packet/send/PacketTowerAllDataRsp.java | 13 +++- .../packet/send/PacketTowerEnterLevelRsp.java | 22 ++++++ .../packet/send/PacketTowerTeamSelectRsp.java | 17 +++++ 22 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 proto/TowerBuffSelectReq.proto create mode 100644 proto/TowerBuffSelectRsp.proto create mode 100644 proto/TowerCurLevelRecordChangeNotify.proto create mode 100644 proto/TowerEnterLevelReq.proto create mode 100644 proto/TowerEnterLevelRsp.proto create mode 100644 proto/TowerLevelStarCondData.proto create mode 100644 proto/TowerLevelStarCondNotify.proto create mode 100644 proto/TowerTeamSelectReq.proto create mode 100644 proto/TowerTeamSelectRsp.proto create mode 100644 src/main/java/emu/grasscutter/data/def/TowerFloorData.java create mode 100644 src/main/java/emu/grasscutter/data/def/TowerLevelData.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java diff --git a/proto/TowerBuffSelectReq.proto b/proto/TowerBuffSelectReq.proto new file mode 100644 index 000000000..1641b350a --- /dev/null +++ b/proto/TowerBuffSelectReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerBuffSelectReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2424; + } + + uint32 tower_buff_id = 1; +} diff --git a/proto/TowerBuffSelectRsp.proto b/proto/TowerBuffSelectRsp.proto new file mode 100644 index 000000000..7a32a8e1e --- /dev/null +++ b/proto/TowerBuffSelectRsp.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerBuffSelectRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2491; + } + + int32 retcode = 1; + uint32 tower_buff_id = 2; +} diff --git a/proto/TowerCurLevelRecordChangeNotify.proto b/proto/TowerCurLevelRecordChangeNotify.proto new file mode 100644 index 000000000..fd9f94c97 --- /dev/null +++ b/proto/TowerCurLevelRecordChangeNotify.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerCurLevelRecord.proto"; + +message TowerCurLevelRecordChangeNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2489; + } + + TowerCurLevelRecord cur_level_record = 1; +} diff --git a/proto/TowerEnterLevelReq.proto b/proto/TowerEnterLevelReq.proto new file mode 100644 index 000000000..551f47729 --- /dev/null +++ b/proto/TowerEnterLevelReq.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerEnterLevelReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2412; + } + + uint32 enter_point_id = 1; +} diff --git a/proto/TowerEnterLevelRsp.proto b/proto/TowerEnterLevelRsp.proto new file mode 100644 index 000000000..fbcc4067f --- /dev/null +++ b/proto/TowerEnterLevelRsp.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerEnterLevelRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2426; + } + + int32 retcode = 1; + uint32 floor_id = 2; + uint32 level_index = 3; + repeated uint32 tower_buff_id_list = 4; +} diff --git a/proto/TowerLevelStarCondData.proto b/proto/TowerLevelStarCondData.proto new file mode 100644 index 000000000..d46334422 --- /dev/null +++ b/proto/TowerLevelStarCondData.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerLevelStarCondData { + uint32 star_cond_index = 3; + uint32 cond_value = 4; + bool is_pause = 5; +} diff --git a/proto/TowerLevelStarCondNotify.proto b/proto/TowerLevelStarCondNotify.proto new file mode 100644 index 000000000..e605496fb --- /dev/null +++ b/proto/TowerLevelStarCondNotify.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerLevelStarCondData.proto"; + +message TowerLevelStarCondNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2492; + } + + uint32 floor_id = 1; + uint32 level_index = 2; + repeated TowerLevelStarCondData cond_data_list = 3; +} diff --git a/proto/TowerTeamSelectReq.proto b/proto/TowerTeamSelectReq.proto new file mode 100644 index 000000000..903968061 --- /dev/null +++ b/proto/TowerTeamSelectReq.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerTeam.proto"; + +message TowerTeamSelectReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 2401; + } + + uint32 floor_id = 1; + repeated TowerTeam tower_team_list = 2; +} diff --git a/proto/TowerTeamSelectRsp.proto b/proto/TowerTeamSelectRsp.proto new file mode 100644 index 000000000..b135e5364 --- /dev/null +++ b/proto/TowerTeamSelectRsp.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerTeamSelectRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2494; + } + + int32 retcode = 1; +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 692427496..76a7f1652 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -68,7 +68,9 @@ public class GameData { private static final Int2ObjectMap<ShopGoodsData> shopGoodsDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<CombineData> combineDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>(); - + private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap = new Int2ObjectOpenHashMap<>(); + // Cache private static Map<Integer, List<Integer>> fetters = new HashMap<>(); private static Map<Integer, List<ShopGoodsData>> shopGoods = new HashMap<>(); @@ -311,4 +313,11 @@ public class GameData { public static Int2ObjectMap<CombineData> getCombineDataMap() { return combineDataMap; } + + public static Int2ObjectMap<TowerFloorData> getTowerFloorDataMap(){ + return towerFloorDataMap; + } + public static Int2ObjectMap<TowerLevelData> getTowerLevelDataMap(){ + return towerLevelDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/def/TowerFloorData.java b/src/main/java/emu/grasscutter/data/def/TowerFloorData.java new file mode 100644 index 000000000..d9d0082c7 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerFloorData.java @@ -0,0 +1,73 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +@ResourceType(name = "TowerFloorExcelConfigData.json") +public class TowerFloorData extends GameResource { + + private int FloorId; + private int FloorIndex; + private int LevelId; + private int OverrideMonsterLevel; + private int TeamNum; + private int FloorLevelConfigId; + + @Override + public int getId() { + return this.FloorId; + } + + @Override + public void onLoad() { + super.onLoad(); + } + + public int getFloorId() { + return FloorId; + } + + public void setFloorId(int floorId) { + FloorId = floorId; + } + + public int getFloorIndex() { + return FloorIndex; + } + + public void setFloorIndex(int floorIndex) { + FloorIndex = floorIndex; + } + + public int getLevelId() { + return LevelId; + } + + public void setLevelId(int levelId) { + LevelId = levelId; + } + + public int getOverrideMonsterLevel() { + return OverrideMonsterLevel; + } + + public void setOverrideMonsterLevel(int overrideMonsterLevel) { + OverrideMonsterLevel = overrideMonsterLevel; + } + + public int getTeamNum() { + return TeamNum; + } + + public void setTeamNum(int teamNum) { + TeamNum = teamNum; + } + + public int getFloorLevelConfigId() { + return FloorLevelConfigId; + } + + public void setFloorLevelConfigId(int floorLevelConfigId) { + FloorLevelConfigId = floorLevelConfigId; + } +} diff --git a/src/main/java/emu/grasscutter/data/def/TowerLevelData.java b/src/main/java/emu/grasscutter/data/def/TowerLevelData.java new file mode 100644 index 000000000..6cc45cc06 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerLevelData.java @@ -0,0 +1,55 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +@ResourceType(name = "TowerLevelExcelConfigData.json") +public class TowerLevelData extends GameResource { + + private int ID; + private int LevelId; + private int LevelIndex; + private int DungeonId; + + @Override + public int getId() { + return this.ID; + } + + @Override + public void onLoad() { + super.onLoad(); + } + + public int getID() { + return ID; + } + + public void setID(int ID) { + this.ID = ID; + } + + public int getLevelId() { + return LevelId; + } + + public void setLevelId(int levelId) { + LevelId = levelId; + } + + public int getLevelIndex() { + return LevelIndex; + } + + public void setLevelIndex(int levelIndex) { + LevelIndex = levelIndex; + } + + public int getDungeonId() { + return DungeonId; + } + + public void setDungeonId(int dungeonId) { + DungeonId = dungeonId; + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index c84ef8a22..e858decf8 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -75,7 +75,8 @@ public class DungeonManager { prevPos.set(entry.getPointData().getTranPos()); } } - + // clean temp team if it has + player.getTeamManager().cleanTemporaryTeam(); // Transfer player back to world player.getWorld().transferPlayerToScene(player, prevScene, prevPos); player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp)); diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index f6ea68dc6..d864f9c34 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -27,6 +27,7 @@ import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.managers.MapMarkManager.*; +import emu.grasscutter.game.tower.TowerManager; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; @@ -89,6 +90,8 @@ public class Player { @Transient private MessageHandler messageHandler; private TeamManager teamManager; + + private TowerManager towerManager; private PlayerGachaInfo gachaInfo; private PlayerProfile playerProfile; private boolean showAvatar; @@ -176,6 +179,7 @@ public class Player { this.nickname = "Traveler"; this.signature = ""; this.teamManager = new TeamManager(this); + this.towerManager = new TowerManager(this); this.birthday = new PlayerBirthday(); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); @@ -389,6 +393,10 @@ public class Player { return this.teamManager; } + public TowerManager getTowerManager() { + return towerManager; + } + public PlayerGachaInfo getGachaInfo() { return gachaInfo; } @@ -1030,6 +1038,9 @@ public class Player { if (this.getProfile().getUid() == 0) { this.getProfile().syncWithCharacter(this); } + if (this.getTowerManager() == null) { + this.towerManager = new TowerManager(this); + } // Check if player object exists in server // TODO - optimize diff --git a/src/main/java/emu/grasscutter/game/player/TeamInfo.java b/src/main/java/emu/grasscutter/game/player/TeamInfo.java index 5c66f1aaa..5794a7913 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamInfo.java +++ b/src/main/java/emu/grasscutter/game/player/TeamInfo.java @@ -18,6 +18,11 @@ public class TeamInfo { this.avatars = new ArrayList<>(Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); } + public TeamInfo(List<Integer> avatars) { + this.name = ""; + this.avatars = avatars; + } + public String getName() { return name; } diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index ff9a3c68d..30418993e 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -1,12 +1,6 @@ package emu.grasscutter.game.player; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Transient; @@ -59,7 +53,13 @@ public class TeamManager { @Transient private final Set<EntityBaseGadget> gadgets; @Transient private final IntSet teamResonances; @Transient private final IntSet teamResonancesConfig; - + + private int useTemporarilyTeamIndex = -1; + /** + * Temporary Team for tower + */ + private List<TeamInfo> temporaryTeam; + public TeamManager() { this.mpTeam = new TeamInfo(); this.avatars = new ArrayList<>(); @@ -125,6 +125,10 @@ public class TeamManager { } public TeamInfo getCurrentTeamInfo() { + if (useTemporarilyTeamIndex >= 0 && + useTemporarilyTeamIndex < temporaryTeam.size()){ + return temporaryTeam.get(useTemporarilyTeamIndex); + } if (this.getPlayer().isInMultiplayer()) { return this.getMpTeam(); } @@ -352,7 +356,51 @@ public class TeamManager { // Packet this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(getPlayer(), teamInfo)); } - + + public void setupTemporaryTeam(List<List<Long>> guidList) { + var team = guidList.stream().map(list -> { + // Sanity checks + if (list.size() == 0 || list.size() > getMaxTeamSize()) { + return null; + } + + // Set team data + LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); + for (int i = 0; i < list.size(); i++) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return null; + } + newTeam.add(avatar); + } + + // convert to avatar ids + return newTeam.stream() + .map(Avatar::getAvatarId) + .toList(); + }) + .filter(Objects::nonNull) + .map(TeamInfo::new) + .toList(); + this.temporaryTeam = team; + } + + public void useTemporaryTeam(int index) { + this.useTemporarilyTeamIndex = index; + updateTeamEntities(null); + } + + public void cleanTemporaryTeam() { + // check if using temporary team + if(useTemporarilyTeamIndex < 0){ + return; + } + + this.useTemporarilyTeamIndex = -1; + this.temporaryTeam = null; + updateTeamEntities(null); + } public synchronized void setCurrentTeam(int teamId) { // if (getPlayer().isInMultiplayer()) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java new file mode 100644 index 000000000..e49a15cc2 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -0,0 +1,40 @@ +package emu.grasscutter.game.tower; + +import dev.morphia.annotations.Entity; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; + +import java.util.List; + +@Entity +public class TowerManager { + private final Player player; + + public TowerManager(Player player) { + this.player = player; + } + private int currentLevel; + private int currentFloor; + + public void teamSelect(int floor, List<List<Long>> towerTeams) { + var floorData = GameData.getTowerFloorDataMap().get(floor); + + this.currentFloor = floorData.getFloorId(); + this.currentLevel = floorData.getLevelId(); + + player.getTeamManager().setupTemporaryTeam(towerTeams); + } + + + public void enterLevel(int enterPointId) { + var levelData = GameData.getTowerLevelDataMap().get(currentLevel); + var id = levelData.getDungeonId(); + // use team user choose + player.getTeamManager().useTemporaryTeam(0); + player.getServer().getDungeonManager() + .enterDungeon(player, enterPointId, id); + + player.getSession().send(new PacketTowerEnterLevelRsp(currentFloor, currentLevel)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java new file mode 100644 index 000000000..163f101ed --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerEnterLevelReq.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerEnterLevelReqOuterClass.TowerEnterLevelReq; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.TowerEnterLevelReq) +public class HandlerTowerEnterLevelReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + TowerEnterLevelReq req = TowerEnterLevelReq.parseFrom(payload); + + //session.send(new PacketTowerCurLevelRecordChangeNotify()); + session.getPlayer().getTowerManager().enterLevel(req.getEnterPointId()); + + //session.send(new PacketTowerLevelStarCondNotify()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java new file mode 100644 index 000000000..6e6705379 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerTeamSelectReq.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerTeamOuterClass; +import emu.grasscutter.net.proto.TowerTeamSelectReqOuterClass.TowerTeamSelectReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketTowerTeamSelectRsp; + +@Opcodes(PacketOpcodes.TowerTeamSelectReq) +public class HandlerTowerTeamSelectReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + TowerTeamSelectReq req = TowerTeamSelectReq.parseFrom(payload); + + var towerTeams = req.getTowerTeamListList().stream() + .map(TowerTeamOuterClass.TowerTeam::getAvatarGuidListList) + .toList(); + + session.getPlayer().getTowerManager().teamSelect(req.getFloorId(), towerTeams); + + session.send(new PacketTowerTeamSelectRsp()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java index 2bd1d0171..d2d2376e6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java @@ -1,19 +1,28 @@ package emu.grasscutter.server.packet.send; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.TowerFloorData; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.TowerAllDataRspOuterClass.TowerAllDataRsp; import emu.grasscutter.net.proto.TowerCurLevelRecordOuterClass.TowerCurLevelRecord; import emu.grasscutter.net.proto.TowerFloorRecordOuterClass.TowerFloorRecord; +import java.util.stream.Collectors; + public class PacketTowerAllDataRsp extends BasePacket { public PacketTowerAllDataRsp() { super(PacketOpcodes.TowerAllDataRsp); - + + var list = GameData.getTowerFloorDataMap().values().stream() + .map(TowerFloorData::getFloorId) + .map(id -> TowerFloorRecord.newBuilder().setFloorId(id).build()) + .collect(Collectors.toList()); + TowerAllDataRsp proto = TowerAllDataRsp.newBuilder() .setTowerScheduleId(29) - .addTowerFloorRecordList(TowerFloorRecord.newBuilder().setFloorId(1001)) + .addAllTowerFloorRecordList(list) .setCurLevelRecord(TowerCurLevelRecord.newBuilder().setIsEmpty(true)) .setNextScheduleChangeTime(Integer.MAX_VALUE) .putFloorOpenTimeMap(1024, 1630486800) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java new file mode 100644 index 000000000..ebb8fb2b2 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerEnterLevelRsp.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerEnterLevelRspOuterClass.TowerEnterLevelRsp; + +public class PacketTowerEnterLevelRsp extends BasePacket { + + public PacketTowerEnterLevelRsp(int floorId, int levelIndex) { + super(PacketOpcodes.TowerEnterLevelRsp); + + TowerEnterLevelRsp proto = TowerEnterLevelRsp.newBuilder() + .setFloorId(floorId) + .setLevelIndex(levelIndex) + .addTowerBuffIdList(4) + .addTowerBuffIdList(28) + .addTowerBuffIdList(18) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java new file mode 100644 index 000000000..445b707cd --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerTeamSelectRsp.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerTeamSelectRspOuterClass.TowerTeamSelectRsp; + +public class PacketTowerTeamSelectRsp extends BasePacket { + + public PacketTowerTeamSelectRsp() { + super(PacketOpcodes.TowerTeamSelectRsp); + + TowerTeamSelectRsp proto = TowerTeamSelectRsp.newBuilder() + .build(); + + this.setData(proto); + } +} From 39a49ae9643ae047b39b5a856bfd717aa54834a9 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Fri, 6 May 2022 14:46:10 +0800 Subject: [PATCH 121/434] Add @Transient for temporary team --- src/main/java/emu/grasscutter/game/player/TeamManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 30418993e..16e8942ad 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -54,11 +54,11 @@ public class TeamManager { @Transient private final IntSet teamResonances; @Transient private final IntSet teamResonancesConfig; - private int useTemporarilyTeamIndex = -1; + @Transient private int useTemporarilyTeamIndex = -1; /** * Temporary Team for tower */ - private List<TeamInfo> temporaryTeam; + @Transient private List<TeamInfo> temporaryTeam; public TeamManager() { this.mpTeam = new TeamInfo(); From 39c932b041e2e9fd18d305b9936fb3103f6a2f90 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 00:05:38 -0700 Subject: [PATCH 122/434] Implementes auto HP recovery at the statues. - Respects player setting. - SP + MP. - Statue has unlimited HP volume (to be updated) --- .../managers/SotSManager/SotSManager.java | 118 ++++++++++++++++++ .../emu/grasscutter/game/player/Player.java | 7 ++ .../HandlerEnterTransPointRegionNotify.java | 28 ++--- 3 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java new file mode 100644 index 000000000..b734f5908 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java @@ -0,0 +1,118 @@ +package emu.grasscutter.game.managers.SotSManager; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.proto.ChangeHpReasonOuterClass; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; + +import java.util.List; + +// Statue of the Seven Manager +public class SotSManager { + + private final Player player; + + public SotSManager(Player player) { + this.player = player; + } + + public boolean getIsAutoRecoveryEnabled() { + return player.getProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE) == 1; + } + + public void setIsAutoRecoveryEnabled(boolean enabled) { + player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, enabled ? 1 : 0); + } + + public int getAutoRecoveryPercentage() { + return player.getProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT); + } + + public void setAutoRecoveryPercentage(int percentage) { + player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, percentage); + } + + // autoRevive automatically revives all team members. + public void autoRevive(GameSession session) { + player.getTeamManager().getActiveTeam().forEach(entity -> { + boolean isAlive = entity.isAlive(); + if (!isAlive) { + float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Math.min(150, maxHP)); + entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + } + }); + } + + public void scheduleAutoRecover(GameSession session) { + // TODO: play audio effects? possibly client side? - client automatically plays. + // delay 2.5 seconds + new Thread(() -> { + try { + Thread.sleep(2500); + autoRecover(session); + } catch (Exception e) { + Grasscutter.getLogger().error(e.getMessage()); + } + }).start(); + } + + // autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level. + public void autoRecover(GameSession session) { + // TODO: Implement SotS Spring volume refill over time. + // Placeholder: + player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000); + player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, 8500000); + + // TODO: In MP, respect SotS settings from the host. + boolean isAutoRecoveryEnabled = getIsAutoRecoveryEnabled(); + int autoRecoverPercentage = getAutoRecoveryPercentage(); + Grasscutter.getLogger().warn("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage); + + if (isAutoRecoveryEnabled) { + player.getTeamManager().getActiveTeam().forEach(entity -> { + float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + if (currentHP == maxHP) { + return; + } + float targetHP = maxHP * autoRecoverPercentage / 100; + + if (targetHP > currentHP) { + float needHP = targetHP - currentHP; + + int sotsHPBalance = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME); + if (sotsHPBalance >= needHP) { + sotsHPBalance -= needHP; + player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, sotsHPBalance); + + float newHP = currentHP + needHP; + + session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_MAX_HP)); + session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP, + newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER, + ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue)); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + + Avatar avatar = entity.getAvatar(); + session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP)); + avatar.setCurrentHp(newHP); + + player.save(); + } + + } + }); + } + } + + +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index d864f9c34..eb45f1390 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -22,6 +22,7 @@ import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.MailHandler; import emu.grasscutter.game.managers.MovementManager.MovementManager; +import emu.grasscutter.game.managers.SotSManager.SotSManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; @@ -89,6 +90,8 @@ public class Player { @Transient private MailHandler mailHandler; @Transient private MessageHandler messageHandler; + @Transient private SotSManager sotsManager; + private TeamManager teamManager; private TowerManager towerManager; @@ -168,6 +171,7 @@ public class Player { this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); this.movementManager = new MovementManager(this); + this.sotsManager = new SotSManager(this); } // On player creation @@ -196,6 +200,7 @@ public class Player { this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); this.movementManager = new MovementManager(this); + this.sotsManager = new SotSManager(this); } public int getUid() { @@ -984,6 +989,8 @@ public class Player { public MovementManager getMovementManager() { return movementManager; } + public SotSManager getSotSManager() { return sotsManager; } + public synchronized void onTick() { // Check ping if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java index 2c946e1fa..0ec0a844f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java @@ -1,5 +1,7 @@ package emu.grasscutter.server.packet.recv; +import emu.grasscutter.game.managers.SotSManager.SotSManager; +import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; @@ -18,26 +20,10 @@ import java.util.List; public class HandlerEnterTransPointRegionNotify extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception{ - session.getPlayer().getTeamManager().getActiveTeam().forEach(entity -> { - boolean isAlive = entity.isAlive(); - if(entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) != entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP)){ - Float hp = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP)-entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - - session.send(new PacketEntityFightPropUpdateNotify(entity,FightProperty.FIGHT_PROP_MAX_HP)); - - session.send(new PacketEntityFightPropChangeReasonNotify( - entity, FightProperty.FIGHT_PROP_CUR_HP, hp, List.of(3), - PropChangeReason.PROP_CHANGE_STATUE_RECOVER, ChangeHpReason.ChangeHpAddStatue)); - - entity.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) - ); - session.send(new PacketAvatarFightPropUpdateNotify(entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - if (!isAlive) { - entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - } - } - }); + Player player = session.getPlayer(); + SotSManager sotsManager = player.getSotSManager(); + sotsManager.autoRevive(session); + sotsManager.scheduleAutoRecover(session); + // TODO: allow interaction with the SotS? } } From e319fd751bd2e79e79345c22e4bf994b89daf68b Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 00:28:35 -0700 Subject: [PATCH 123/434] fix: lower logging level in SotSManager --- .../emu/grasscutter/game/managers/SotSManager/SotSManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java index b734f5908..dec54f686 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java @@ -75,7 +75,7 @@ public class SotSManager { // TODO: In MP, respect SotS settings from the host. boolean isAutoRecoveryEnabled = getIsAutoRecoveryEnabled(); int autoRecoverPercentage = getAutoRecoveryPercentage(); - Grasscutter.getLogger().warn("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage); + Grasscutter.getLogger().debug("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage); if (isAutoRecoveryEnabled) { player.getTeamManager().getActiveTeam().forEach(entity -> { From 5968ed3a71d147caab5fa4aa768ff2e5de0c2eef Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 01:17:16 -0700 Subject: [PATCH 124/434] Remove the red exclamation mark from achievements --- .../packet/send/PacketTakeAchievementRewardReq.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTakeAchievementRewardReq.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTakeAchievementRewardReq.java index 24135d52c..66049c64c 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTakeAchievementRewardReq.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTakeAchievementRewardReq.java @@ -15,14 +15,7 @@ public class PacketTakeAchievementRewardReq extends BasePacket { public PacketTakeAchievementRewardReq(GameSession session) { super(PacketOpcodes.TakeAchievementRewardReq); - List<AchievementInfo> a_list = new ArrayList<>(); - a_list.add(AchievementInfo.newBuilder().setId(82044).setStatusValue(2).setCurrent(0).setGoal(1).build()); - a_list.add(AchievementInfo.newBuilder().setId(81205).setStatusValue(2).setCurrent(0).setGoal(1).build()); - - - TakeAchievementRewardReq proto = TakeAchievementRewardReq.newBuilder() - .addAllAList(a_list) - .build(); + TakeAchievementRewardReq proto = TakeAchievementRewardReq.newBuilder().build(); this.setData(proto); } From 098cf372c91492e98da050bcb67071321d7bbe53 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 01:19:39 -0700 Subject: [PATCH 125/434] Fix morphia error when saving player to db --- src/main/java/emu/grasscutter/game/tower/TowerManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index e49a15cc2..3b45785dd 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -1,6 +1,7 @@ package emu.grasscutter.game.tower; import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; import emu.grasscutter.data.GameData; import emu.grasscutter.game.player.Player; import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; @@ -9,11 +10,12 @@ import java.util.List; @Entity public class TowerManager { - private final Player player; + @Transient private final Player player; public TowerManager(Player player) { this.player = player; } + private int currentLevel; private int currentFloor; From 98e1189deb781c87bf81bf1005fac8b760d8a403 Mon Sep 17 00:00:00 2001 From: Bwly999 <438225686@qq.com> Date: Fri, 6 May 2022 18:16:07 +0800 Subject: [PATCH 126/434] fix the problem that the reference of serverHook in Plugin object is null --- .../java/emu/grasscutter/Grasscutter.java | 5 ++- .../grasscutter/server/game/GameServer.java | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index a1c8a5c7c..81e729b2c 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -92,14 +92,13 @@ public final class Grasscutter { // Database DatabaseManager.initialize(); - // Create plugin manager instance. - pluginManager = new PluginManager(); - // Create server instances. dispatchServer = new DispatchServer(); gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); // Create a server hook instance with both servers. new ServerHook(gameServer, dispatchServer); + // Create plugin manager instance. + pluginManager = new PluginManager(); // Start servers. if (getConfig().RunMode == ServerRunMode.HYBRID) { diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index db4a8c32b..0135145c0 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -28,6 +28,9 @@ import java.net.InetSocketAddress; import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public final class GameServer extends KcpServer { private final InetSocketAddress address; @@ -67,19 +70,6 @@ public final class GameServer extends KcpServer { this.taskMap = new TaskMap(true); this.dropManager = new DropManager(this); this.combineManger = new CombineManger(this); - - // Schedule game loop. - Timer gameLoop = new Timer(); - gameLoop.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - try { - onTick(); - } catch (Exception e) { - Grasscutter.getLogger().error(Grasscutter.getLanguage().An_error_occurred_during_game_update, e); - } - } - }, new Date(), 1000L); // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); @@ -212,6 +202,23 @@ public final class GameServer extends KcpServer { } + @Override + public synchronized void start() { + // Schedule game loop. + ScheduledExecutorService gameLoop = Executors.newScheduledThreadPool(2); + gameLoop.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + onTick(); + } catch (Exception e) { + Grasscutter.getLogger().error(Grasscutter.getLanguage().An_error_occurred_during_game_update, e); + } + } + }, 0L, 1000L, TimeUnit.MILLISECONDS); + super.start(); + } + @Override public void onStartFinish() { Grasscutter.getLogger().info(Grasscutter.getLanguage().Grasscutter_is_free); From 0102a3ce1e83182f0ef04380155c6541ee3cfa0b Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 02:23:10 -0700 Subject: [PATCH 127/434] The statues will now automatically regen their HP volume over time. Max is currently set to 85000 for everyone. Will update after implementing statue levels. --- .../managers/SotSManager/SotSManager.java | 79 +++++++++++++------ .../emu/grasscutter/game/player/Player.java | 10 +++ .../HandlerEnterTransPointRegionNotify.java | 2 + 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java index dec54f686..4edad231e 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java @@ -18,6 +18,8 @@ import java.util.List; // Statue of the Seven Manager public class SotSManager { + // NOTE: Spring volume balance *1 = fight prop HP *100 + private final Player player; public SotSManager(Player player) { @@ -46,7 +48,8 @@ public class SotSManager { boolean isAlive = entity.isAlive(); if (!isAlive) { float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Math.min(150, maxHP)); + float newHP = (float)(maxHP * 0.3); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); } }); @@ -65,14 +68,31 @@ public class SotSManager { }).start(); } + public void refillSpringVolume() { + // TODO: max spring volume depends on level of the statues in Mondstadt and Liyue. + // https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking + player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000); + + long now = System.currentTimeMillis() / 1000; + long secondsSinceLastUsed = now - player.getSpringLastUsed(); + float percentageRefilled = (float)secondsSinceLastUsed / 15 / 100; // 15s = 1% max volume + int maxVolume = player.getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME); + int currentVolume = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME); + if (currentVolume < maxVolume) { + int volumeRefilled = (int)(percentageRefilled * maxVolume); + int newVolume = currentVolume + volumeRefilled; + if (currentVolume + volumeRefilled > maxVolume) { + newVolume = maxVolume; + } + player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, newVolume); + } + player.setSpringLastUsed(now); + player.save(); + } + // autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level. public void autoRecover(GameSession session) { - // TODO: Implement SotS Spring volume refill over time. - // Placeholder: - player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000); - player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, 8500000); - - // TODO: In MP, respect SotS settings from the host. + // TODO: In MP, respect SotS settings from the HOST. boolean isAutoRecoveryEnabled = getIsAutoRecoveryEnabled(); int autoRecoverPercentage = getAutoRecoveryPercentage(); Grasscutter.getLogger().debug("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage); @@ -88,27 +108,36 @@ public class SotSManager { if (targetHP > currentHP) { float needHP = targetHP - currentHP; + float needSV = needHP * 100; // convert HP needed to Spring Volume needed - int sotsHPBalance = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME); - if (sotsHPBalance >= needHP) { - sotsHPBalance -= needHP; - player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, sotsHPBalance); - - float newHP = currentHP + needHP; - - session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_MAX_HP)); - session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP, - newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER, - ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue)); - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); - - Avatar avatar = entity.getAvatar(); - session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP)); - avatar.setCurrentHp(newHP); - - player.save(); + int sotsSVBalance = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME); + if (sotsSVBalance >= needSV) { + // sufficient + sotsSVBalance -= needSV; + } else { + // insufficient balance + needSV = sotsSVBalance; + sotsSVBalance = 0; } + player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, sotsSVBalance); + player.setSpringLastUsed(System.currentTimeMillis() / 1000); + float newHP = currentHP + needSV / 100; // convert SV to HP + + // TODO: Figure out why client shows current HP instead of added HP. + // Say an avatar had 12000 and now has 14000, it should show "2000". + // The client always show "+14000" which is incorrect. + + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP, + newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER, + ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue)); + session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + + Avatar avatar = entity.getAvatar(); + avatar.setCurrentHp(newHP); + session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP)); + player.save(); } }); } diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index eb45f1390..b93baccf8 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -130,6 +130,8 @@ public class Player { private MapMarksManager mapMarksManager; @Transient private MovementManager movementManager; + private long springLastUsed; + @Deprecated @SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only! @@ -535,6 +537,14 @@ public class Player { } } + public long getSpringLastUsed() { + return springLastUsed; + } + + public void setSpringLastUsed(long val) { + springLastUsed = val; + } + public SceneLoadState getSceneLoadState() { return sceneState; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java index 0ec0a844f..5591607fe 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java @@ -22,6 +22,8 @@ public class HandlerEnterTransPointRegionNotify extends PacketHandler { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception{ Player player = session.getPlayer(); SotSManager sotsManager = player.getSotSManager(); + + sotsManager.refillSpringVolume(); sotsManager.autoRevive(session); sotsManager.scheduleAutoRecover(session); // TODO: allow interaction with the SotS? From da99140d2066392d2b236acde854b38a2e133caa Mon Sep 17 00:00:00 2001 From: 4Benj_ <Benjamin7006@gmail.com> Date: Fri, 6 May 2022 21:48:16 +0800 Subject: [PATCH 128/434] Stop WindSeedClientNotify and PlayerLuaShellNotify from being sent (#582) --- src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java | 5 +++++ src/main/java/emu/grasscutter/server/game/GameSession.java | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java index 4d9eb57e8..8e77504d6 100644 --- a/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java +++ b/src/main/java/emu/grasscutter/net/packet/PacketOpcodes.java @@ -1,5 +1,8 @@ package emu.grasscutter.net.packet; +import java.util.Arrays; +import java.util.List; + public class PacketOpcodes { // Empty public static final int NONE = 0; @@ -1566,4 +1569,6 @@ public class PacketOpcodes { public static final int UNKNOWN_43 = 8877; public static final int UNKNOWN_44 = 8983; public static final int UNKNOWN_45 = 943; + + public static final List<Integer> BANNED_PACKETS = Arrays.asList(PacketOpcodes.WindSeedClientNotify, PacketOpcodes.PlayerLuaShellNotify); } diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index ff024b03b..b984baa0e 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -157,6 +157,12 @@ public class GameSession extends KcpChannel { Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!"); return; } + + // DO NOT REMOVE (unless we find a way to validate code before sending to client which I don't think we can) + // Stop WindSeedClientNotify from being sent for security purposes. + if(PacketOpcodes.BANNED_PACKETS.contains(packet.getOpcode())) { + return; + } // Header if (packet.shouldBuildHeader()) { From 6144f4712789c35d357daae7003cf3609cb760f6 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 00:11:54 +0800 Subject: [PATCH 129/434] Tower Dungeons Handoff between level and floor --- proto/DungeonSettleNotify.proto | 5 ++ proto/TowerFloorRecordChangeNotify.proto | 18 +++++ proto/TowerLevelEndNotify.proto | 26 +++++++ .../dungeons/BasicDungeonSettleListener.java | 14 ++++ .../game/dungeons/DungeonChallenge.java | 19 ++--- .../game/dungeons/DungeonManager.java | 28 ++++++- .../game/dungeons/DungeonSettleListener.java | 7 ++ .../dungeons/TowerDungeonSettleListener.java | 24 ++++++ .../emu/grasscutter/game/player/Player.java | 6 ++ .../grasscutter/game/tower/TowerManager.java | 78 ++++++++++++++++--- .../emu/grasscutter/game/world/Scene.java | 17 +++- .../send/PacketDungeonSettleNotify.java | 41 ++++++++++ ...PacketTowerCurLevelRecordChangeNotify.java | 23 ++++++ .../PacketTowerFloorRecordChangeNotify.java | 30 +++++++ 14 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 proto/TowerFloorRecordChangeNotify.proto create mode 100644 proto/TowerLevelEndNotify.proto create mode 100644 src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java create mode 100644 src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java create mode 100644 src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerCurLevelRecordChangeNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java diff --git a/proto/DungeonSettleNotify.proto b/proto/DungeonSettleNotify.proto index c190b38e7..c48ab8f6f 100644 --- a/proto/DungeonSettleNotify.proto +++ b/proto/DungeonSettleNotify.proto @@ -4,8 +4,13 @@ option java_package = "emu.grasscutter.net.proto"; import "ParamList.proto"; import "StrengthenPointData.proto"; +import "TowerLevelEndNotify.proto"; message DungeonSettleNotify { + oneof Detail { + TowerLevelEndNotify tower_level_end_notify = 101; + // it has more! + } uint32 dungeon_id = 1; bool is_success = 2; repeated uint32 fail_cond_list = 3; diff --git a/proto/TowerFloorRecordChangeNotify.proto b/proto/TowerFloorRecordChangeNotify.proto new file mode 100644 index 000000000..74a7135ec --- /dev/null +++ b/proto/TowerFloorRecordChangeNotify.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "TowerFloorRecord.proto"; + +message TowerFloorRecordChangeNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2418; + } + + repeated TowerFloorRecord tower_floor_record_list = 1; + bool is_finished_entrance_floor = 2; +} diff --git a/proto/TowerLevelEndNotify.proto b/proto/TowerLevelEndNotify.proto new file mode 100644 index 000000000..d9f2da543 --- /dev/null +++ b/proto/TowerLevelEndNotify.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "ItemParam.proto"; + +message TowerLevelEndNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2456; + } + + enum ContinueStateType { + CONTINUE_STATE_CAN_NOT_CONTINUE = 0; + CONTINUE_STATE_CAN_ENTER_NEXT_LEVEL = 1; + CONTINUE_STATE_CAN_ENTER_NEXT_FLOOR = 2; + } + + bool is_success = 1; + repeated uint32 finished_star_cond_list = 2; + repeated ItemParam reward_item_list = 3; + uint32 continue_state = 4; + uint32 next_floor_id = 5; +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java new file mode 100644 index 000000000..5b5e243bd --- /dev/null +++ b/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java @@ -0,0 +1,14 @@ +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; +import emu.grasscutter.utils.Utils; + +public class BasicDungeonSettleListener implements DungeonSettleListener { + + @Override + public void onDungeonSettle(Scene scene) { + scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); + scene.broadcastPacket(new PacketDungeonSettleNotify(scene.getChallenge())); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java index b9724f49a..2e07f0058 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java @@ -1,33 +1,25 @@ package emu.grasscutter.game.dungeons; -import java.util.ArrayList; -import java.util.List; - -import emu.grasscutter.data.GameData; import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.data.def.DungeonData; -import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.ScriptArgs; import emu.grasscutter.server.packet.send.PacketChallengeDataNotify; import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify; import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; -import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; -import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; -import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.ArrayList; +import java.util.List; + public class DungeonChallenge { private final Scene scene; private final SceneGroup group; @@ -40,7 +32,7 @@ public class DungeonChallenge { private int score; private int objective = 0; private IntSet rewardedPlayers; - + public DungeonChallenge(Scene scene, SceneGroup group) { this.scene = scene; this.group = group; @@ -129,8 +121,7 @@ public class DungeonChallenge { } private void settle() { - getScene().setAutoCloseTime(Utils.getCurrentSeconds() + 1000); - getScene().broadcastPacket(new PacketDungeonSettleNotify(this)); + getScene().getDungeonSettleObservers().forEach(o -> o.onDungeonSettle(getScene())); getScene().getScriptManager().callEvent(EventType.EVENT_DUNGEON_SETTLE, new ScriptArgs(this.isSuccess() ? 1 : 0)); } diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index e858decf8..f4b16c811 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -14,9 +14,11 @@ import emu.grasscutter.server.packet.send.PacketDungeonEntryInfoRsp; import emu.grasscutter.server.packet.send.PacketPlayerEnterDungeonRsp; import emu.grasscutter.utils.Position; +import java.util.List; + public class DungeonManager { private final GameServer server; - + private static final BasicDungeonSettleListener basicDungeonSettleObserver = new BasicDungeonSettleListener(); public DungeonManager(GameServer server) { this.server = server; } @@ -49,13 +51,32 @@ public class DungeonManager { int sceneId = data.getSceneId(); player.getScene().setPrevScene(sceneId); - player.getWorld().transferPlayerToScene(player, sceneId, data); + if(player.getWorld().transferPlayerToScene(player, sceneId, data)){ + player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver); + } player.getScene().setPrevScenePoint(pointId); player.sendPacket(new PacketPlayerEnterDungeonRsp(pointId, dungeonId)); return true; } - + + /** + * used in tower dungeons handoff + */ + public boolean handoffDungeon(Player player, int dungeonId, List<DungeonSettleListener> dungeonSettleListeners) { + DungeonData data = GameData.getDungeonDataMap().get(dungeonId); + + if (data == null) { + return false; + } + Grasscutter.getLogger().info(player.getNickname() + " is trying to enter tower dungeon " + dungeonId); + + if(player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)){ + dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver); + } + return true; + } + public void exitDungeon(Player player) { if (player.getScene().getSceneType() != SceneType.SCENE_DUNGEON) { return; @@ -77,6 +98,7 @@ public class DungeonManager { } // clean temp team if it has player.getTeamManager().cleanTemporaryTeam(); + player.getTowerManager().clearEntry(); // Transfer player back to world player.getWorld().transferPlayerToScene(player, prevScene, prevPos); player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp)); diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java new file mode 100644 index 000000000..2eb389e05 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java @@ -0,0 +1,7 @@ +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.world.Scene; + +public interface DungeonSettleListener { + void onDungeonSettle(Scene scene); +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java new file mode 100644 index 000000000..5b1ff7a30 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -0,0 +1,24 @@ +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; +import emu.grasscutter.server.packet.send.PacketTowerFloorRecordChangeNotify; +import emu.grasscutter.utils.Utils; + +public class TowerDungeonSettleListener implements DungeonSettleListener { + + @Override + public void onDungeonSettle(Scene scene) { + scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); + var towerManager = scene.getPlayers().get(0).getTowerManager(); + + towerManager.notifyCurLevelRecordChangeWhenDone(); + scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify(towerManager.getCurrentFloorId())); + scene.broadcastPacket(new PacketDungeonSettleNotify(scene.getChallenge(), + true, + towerManager.hasNextLevel(), + towerManager.getNextFloorId() + )); + + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 971fbf2ac..7a09f1ecb 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -25,6 +25,7 @@ import emu.grasscutter.game.managers.MotionManager.MotionManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.managers.MapMarkManager.*; import emu.grasscutter.game.tower.TowerManager; @@ -1048,6 +1049,7 @@ public class Player { @PostLoad private void onLoad() { this.getTeamManager().setPlayer(this); + this.getTowerManager().setPlayer(this); } public void save() { @@ -1117,6 +1119,10 @@ public class Player { } public void onLogout() { + // force to leave the dungeon + if(getScene().getSceneType() == SceneType.SCENE_DUNGEON){ + this.getServer().getDungeonManager().exitDungeon(this); + } // Leave world if (this.getWorld() != null) { this.getWorld().removePlayer(this); diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index e49a15cc2..144443c87 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -1,40 +1,100 @@ package emu.grasscutter.game.tower; import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.TowerLevelData; +import emu.grasscutter.game.dungeons.DungeonSettleListener; +import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; import java.util.List; @Entity public class TowerManager { - private final Player player; + @Transient + private Player player; public TowerManager(Player player) { this.player = player; } - private int currentLevel; - private int currentFloor; + public void setPlayer(Player player) { + this.player = player; + } + private int currentFloorId; + private int currentLevel; + @Transient + private int currentLevelId; + + @Transient + private int entryScene; + + public int getCurrentFloorId() { + return currentFloorId; + } + + private static final List<DungeonSettleListener> towerDungeonSettleListener = List.of(new TowerDungeonSettleListener()); public void teamSelect(int floor, List<List<Long>> towerTeams) { var floorData = GameData.getTowerFloorDataMap().get(floor); - this.currentFloor = floorData.getFloorId(); - this.currentLevel = floorData.getLevelId(); + this.currentFloorId = floorData.getFloorId(); + this.currentLevel = 0; + this.currentLevelId = GameData.getTowerLevelDataMap().values().stream() + .filter(x -> x.getLevelId() == floorData.getLevelId() && x.getLevelIndex() == 1) + .findFirst() + .map(TowerLevelData::getID) + .orElse(0); + + if (entryScene == 0){ + entryScene = player.getSceneId(); + } player.getTeamManager().setupTemporaryTeam(towerTeams); } public void enterLevel(int enterPointId) { - var levelData = GameData.getTowerLevelDataMap().get(currentLevel); + var levelData = GameData.getTowerLevelDataMap().get(currentLevelId + currentLevel); + + this.currentLevel++; var id = levelData.getDungeonId(); + + notifyCurLevelRecordChange(); // use team user choose player.getTeamManager().useTemporaryTeam(0); - player.getServer().getDungeonManager() - .enterDungeon(player, enterPointId, id); + player.getServer().getDungeonManager().handoffDungeon(player, id, + towerDungeonSettleListener); - player.getSession().send(new PacketTowerEnterLevelRsp(currentFloor, currentLevel)); + // make sure user can exit dungeon correctly + player.getScene().setPrevScene(entryScene); + player.getScene().setPrevScenePoint(enterPointId); + + player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, currentLevel)); + + } + + public void notifyCurLevelRecordChange(){ + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel)); + } + public void notifyCurLevelRecordChangeWhenDone(){ + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel + 1)); + } + public boolean hasNextLevel(){ + return this.currentLevel < 3; + } + + public int getNextFloorId() { + if(hasNextLevel()){ + return 0; + } + this.currentFloorId++; + return this.currentFloorId; + } + + public void clearEntry() { + this.entryScene = 0; } } diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 7aa4d3f8a..97099c9b9 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -1,6 +1,5 @@ package emu.grasscutter.game.world; -import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameDepot; import emu.grasscutter.data.def.DungeonData; @@ -8,6 +7,7 @@ import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.SceneData; import emu.grasscutter.data.def.WorldLevelData; import emu.grasscutter.game.dungeons.DungeonChallenge; +import emu.grasscutter.game.dungeons.DungeonSettleListener; import emu.grasscutter.game.entity.*; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.TeamInfo; @@ -20,11 +20,8 @@ import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; import emu.grasscutter.scripts.SceneScriptManager; -import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneBlock; -import emu.grasscutter.scripts.data.SceneGadget; import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.ScriptArgs; import emu.grasscutter.server.packet.send.PacketAvatarSkillInfoNotify; import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; @@ -56,6 +53,7 @@ public class Scene { private SceneScriptManager scriptManager; private DungeonChallenge challenge; + private List<DungeonSettleListener> dungeonSettleListeners; private DungeonData dungeonData; private int prevScene; // Id of the previous scene private int prevScenePoint; @@ -205,6 +203,17 @@ public class Scene { this.challenge = challenge; } + public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener){ + if(dungeonSettleListeners == null){ + dungeonSettleListeners = new ArrayList<>(); + } + dungeonSettleListeners.add(dungeonSettleListener); + } + + public List<DungeonSettleListener> getDungeonSettleObservers() { + return dungeonSettleListeners; + } + public boolean isInScene(GameEntity entity) { return this.entities.containsKey(entity.getId()); } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java index 411be291a..479029243 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java @@ -4,6 +4,8 @@ import emu.grasscutter.game.dungeons.DungeonChallenge; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.DungeonSettleNotifyOuterClass.DungeonSettleNotify; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.TowerLevelEndNotifyOuterClass.TowerLevelEndNotify; public class PacketDungeonSettleNotify extends BasePacket { @@ -19,4 +21,43 @@ public class PacketDungeonSettleNotify extends BasePacket { this.setData(proto); } + + public PacketDungeonSettleNotify(DungeonChallenge challenge, + boolean canJump, + boolean hasNextLevel, + int nextFloorId + ) { + super(PacketOpcodes.DungeonSettleNotify); + + var continueStatus = TowerLevelEndNotify.ContinueStateType.CONTINUE_STATE_CAN_NOT_CONTINUE_VALUE; + if(challenge.isSuccess() && canJump){ + continueStatus = hasNextLevel ? TowerLevelEndNotify.ContinueStateType.CONTINUE_STATE_CAN_ENTER_NEXT_LEVEL_VALUE + : TowerLevelEndNotify.ContinueStateType.CONTINUE_STATE_CAN_ENTER_NEXT_FLOOR_VALUE; + } + + var towerLevelEndNotify = TowerLevelEndNotify.newBuilder() + .setIsSuccess(challenge.isSuccess()) + .setContinueState(continueStatus) + .addFinishedStarCondList(1) + .addFinishedStarCondList(2) + .addFinishedStarCondList(3) + .addRewardItemList(ItemParamOuterClass.ItemParam.newBuilder() + .setItemId(201) + .setCount(1000) + .build()) + ; + if(nextFloorId > 0){ + towerLevelEndNotify.setNextFloorId(nextFloorId); + } + + DungeonSettleNotify proto = DungeonSettleNotify.newBuilder() + .setDungeonId(challenge.getScene().getDungeonData().getId()) + .setIsSuccess(challenge.isSuccess()) + .setCloseTime(challenge.getScene().getAutoCloseTime()) + .setResult(challenge.isSuccess() ? 1 : 0) + .setTowerLevelEndNotify(towerLevelEndNotify.build()) + .build(); + + this.setData(proto); + } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerCurLevelRecordChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerCurLevelRecordChangeNotify.java new file mode 100644 index 000000000..fdae92555 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerCurLevelRecordChangeNotify.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerCurLevelRecordChangeNotifyOuterClass.TowerCurLevelRecordChangeNotify; +import emu.grasscutter.net.proto.TowerCurLevelRecordOuterClass.TowerCurLevelRecord; + +public class PacketTowerCurLevelRecordChangeNotify extends BasePacket { + + public PacketTowerCurLevelRecordChangeNotify(int curFloorId, int curLevelIndex) { + super(PacketOpcodes.TowerCurLevelRecordChangeNotify); + + TowerCurLevelRecordChangeNotify proto = TowerCurLevelRecordChangeNotify.newBuilder() + .setCurLevelRecord(TowerCurLevelRecord.newBuilder() + .setCurFloorId(curFloorId) + .setCurLevelIndex(curLevelIndex) + // TODO team info + .build()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java new file mode 100644 index 000000000..c0ed414a8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java @@ -0,0 +1,30 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerFloorRecordChangeNotifyOuterClass.TowerFloorRecordChangeNotify; +import emu.grasscutter.net.proto.TowerFloorRecordOuterClass.TowerFloorRecord; +import emu.grasscutter.net.proto.TowerLevelRecordOuterClass.TowerLevelRecord; + +public class PacketTowerFloorRecordChangeNotify extends BasePacket { + + public PacketTowerFloorRecordChangeNotify(int floorId) { + super(PacketOpcodes.TowerFloorRecordChangeNotify); + + TowerFloorRecordChangeNotify proto = TowerFloorRecordChangeNotify.newBuilder() + .addTowerFloorRecordList(TowerFloorRecord.newBuilder() + .setFloorId(floorId) + .setFloorStarRewardProgress(3) + .addPassedLevelRecordList(TowerLevelRecord.newBuilder() + .setLevelId(1) + .addSatisfiedCondList(1) + .addSatisfiedCondList(2) + .addSatisfiedCondList(3) + .build()) + .build()) + .setIsFinishedEntranceFloor(true) + .build(); + + this.setData(proto); + } +} From 81ca86092c17289f16e63ef52f67e47db444a662 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 00:15:23 +0800 Subject: [PATCH 130/434] Add some lua functions --- .../java/emu/grasscutter/scripts/ScriptLib.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index e75920579..941b00b60 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -207,4 +207,19 @@ public class ScriptLib { public void PrintContextLog(String msg) { Grasscutter.getLogger().info("[LUA] " + msg); } + + public int TowerCountTimeStatus(int var1, int var2){ + return 0; + } + public int GetGroupMonsterCount(int var1){ + // Maybe... + return GetGroupMonsterCountByGroupId(var1); + } + public int SetMonsterBattleByGroup(int var1, int var2, int var3){ + return 0; + } + + public int CauseDungeonFail(int var1){ + return 0; + } } From 22df34606c41ed81b6a8433377158a1f79a836d3 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 00:31:48 +0800 Subject: [PATCH 131/434] a little fix --- src/main/java/emu/grasscutter/game/tower/TowerManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 116861dd8..51f840663 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -15,9 +15,7 @@ import java.util.List; @Entity public class TowerManager { - - @Transient private final Player player; - + @Transient private Player player; public TowerManager(Player player) { this.player = player; From 63a37acc1b46d44bec29f9bd4485fdf648436a53 Mon Sep 17 00:00:00 2001 From: Muhammad Eko Prasetyo <muhammadekoprasetyo29@gmail.com> Date: Sat, 7 May 2022 03:23:26 +0700 Subject: [PATCH 132/434] add config option to enable cors for dispatchserver (#579) --- src/main/java/emu/grasscutter/Config.java | 2 ++ .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index e7536b588..c911a1cf0 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -41,6 +41,8 @@ public final class Config { public String KeystorePassword = "123456"; public Boolean UseSSL = true; public Boolean FrontHTTPS = true; + public Boolean CORS = false; + public String[] CORSAllowedOrigins = new String[] { "*" }; public boolean AutomaticallyCreateAccounts = false; public String[] defaultPermissions = new String[] { "" }; diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 85f02a36d..737193fbd 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -245,8 +245,11 @@ public final class DispatchServer { if(Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { config.enableDevLogging(); } + if (Grasscutter.getConfig().getDispatchOptions().CORS){ + if (Grasscutter.getConfig().getDispatchOptions().CORSAllowedOrigins.length > 0) config.enableCorsForOrigin(Grasscutter.getConfig().getDispatchOptions().CORSAllowedOrigins); + else config.enableCorsForAllOrigins(); + } }); - httpServer.get("/", (req, res) -> res.send(Grasscutter.getLanguage().Welcome)); httpServer.raw().error(404, ctx -> { From 19a2c9b7ea6d73e7d0087d68095fadb86cbee982 Mon Sep 17 00:00:00 2001 From: Kinesis <CCasusensa@users.noreply.github.com> Date: Fri, 6 May 2022 22:40:16 +0800 Subject: [PATCH 133/434] Implement Avatar Expedition System MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: ShigemoriHakura <62388797+ShigemoriHakura@users.noreply.github.com> Co-Authored-By: KanyeWestc <104547412+KanyeWestc@users.noreply.github.com> Co-Authored-By: QAQ 天小络 <72185326+XTL676@users.noreply.github.com> Co-Authored-By: nkxingxh <25559053+nkxingxh@users.noreply.github.com> Co-Authored-By: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Co-Authored-By: wuwuwu223 <81224214+wuwuwu223@users.noreply.github.com> Co-Authored-By: omg-xtao <100690902+omg-xtao@users.noreply.github.com> Co-Authored-By: Sakura <104815797+Sakura@users.noreply.github.com> Co-Authored-By: NewNeko-2022 <104819344+NewNeko-2022@users.noreply.github.com> Co-Authored-By: JimWails <30657653+JimWails@users.noreply.github.com> Co-Authored-By: buttercookies <19878609+ButterCookies@users.noreply.github.com> --- data/ExpeditionReward.json | 112 ++++++++++++++++++ .../game/expedition/ExpeditionInfo.java | 44 +++++++ .../game/expedition/ExpeditionManager.java | 46 +++++++ .../game/expedition/ExpeditionRewardData.java | 18 +++ .../expedition/ExpeditionRewardDataList.java | 15 +++ .../game/expedition/ExpeditionRewardInfo.java | 16 +++ .../emu/grasscutter/game/player/Player.java | 42 +++++++ .../grasscutter/server/game/GameServer.java | 10 +- .../HandlerAvatarExpeditionAllDataReq.java | 15 +++ .../HandlerAvatarExpeditionCallBackReq.java | 25 ++++ .../HandlerAvatarExpeditionGetRewardReq.java | 61 ++++++++++ .../recv/HandlerAvatarExpeditionStartReq.java | 23 ++++ .../PacketAvatarExpeditionAllDataRsp.java | 33 ++++++ .../PacketAvatarExpeditionCallBackRsp.java | 23 ++++ .../PacketAvatarExpeditionDataNotify.java | 29 +++++ .../PacketAvatarExpeditionGetRewardRsp.java | 30 +++++ .../send/PacketAvatarExpeditionStartRsp.java | 23 ++++ 17 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 data/ExpeditionReward.json create mode 100644 src/main/java/emu/grasscutter/game/expedition/ExpeditionInfo.java create mode 100644 src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java create mode 100644 src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardData.java create mode 100644 src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardDataList.java create mode 100644 src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardInfo.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionAllDataReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionCallBackReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionGetRewardReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionStartReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionAllDataRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionCallBackRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionDataNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionGetRewardRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionStartRsp.java diff --git a/data/ExpeditionReward.json b/data/ExpeditionReward.json new file mode 100644 index 000000000..90d9378b6 --- /dev/null +++ b/data/ExpeditionReward.json @@ -0,0 +1,112 @@ +[ + { + "expId": 301, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 625, + "maxCount": 625 + } + ] + }, + { + "hourTime": 8, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 1575, + "maxCount": 1575 + } + ] + }, + { + "hourTime": 12, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 2500, + "maxCount": 2500 + } + ] + }, + { + "hourTime": 20, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 5000, + "maxCount": 5000 + } + ] + } + ] + }, + { + "expId": 305, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100064, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 101210, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100064, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 101210, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100064, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 101210, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100064, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 101210, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + } +] \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionInfo.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionInfo.java new file mode 100644 index 000000000..867d692cf --- /dev/null +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionInfo.java @@ -0,0 +1,44 @@ +package emu.grasscutter.game.expedition; + +import dev.morphia.annotations.Entity; + +@Entity +public class ExpeditionInfo { + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + public int getExpId() { + return expId; + } + + public void setExpId(int expId) { + this.expId = expId; + } + + public int getHourTime() { + return hourTime; + } + + public void setHourTime(int hourTime) { + this.hourTime = hourTime; + } + + public int getStartTime() { + return startTime; + } + + public void setStartTime(int startTime) { + this.startTime = startTime; + } + + private int state; + private int expId; + private int hourTime; + private int startTime; +} diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java new file mode 100644 index 000000000..5d1b652e1 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java @@ -0,0 +1,46 @@ +package emu.grasscutter.game.expedition; + +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.game.GameServer; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.io.FileReader; +import java.util.Collection; +import java.util.List; + +public class ExpeditionManager { + public GameServer getGameServer() { + return gameServer; + } + + private final GameServer gameServer; + + public Int2ObjectMap<List<ExpeditionRewardDataList>> getExpeditionRewardDataList() { return expeditionRewardData; } + + private final Int2ObjectMap<List<ExpeditionRewardDataList>> expeditionRewardData; + + public ExpeditionManager(GameServer gameServer) { + this.gameServer = gameServer; + this.expeditionRewardData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public synchronized void load() { + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "ExpeditionReward.json")) { + getExpeditionRewardDataList().clear(); + List<ExpeditionRewardInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType()); + if(banners.size() > 0) { + for (ExpeditionRewardInfo di : banners) { + getExpeditionRewardDataList().put(di.getExpId(), di.getExpeditionRewardDataList()); + } + Grasscutter.getLogger().info("Expedition reward successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load expedition reward. Expedition reward size is 0."); + } + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load expedition reward.", e); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardData.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardData.java new file mode 100644 index 000000000..ded817a3e --- /dev/null +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardData.java @@ -0,0 +1,18 @@ +package emu.grasscutter.game.expedition; + +public class ExpeditionRewardData { + private int itemId; + private int minCount; + private int maxCount; + + public int getItemId() { + return itemId; + } + + public int getMinCount() { return minCount; } + + public int getMaxCount() { + return maxCount; + } + +} diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardDataList.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardDataList.java new file mode 100644 index 000000000..27a7534e2 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardDataList.java @@ -0,0 +1,15 @@ +package emu.grasscutter.game.expedition; + +import java.util.List; + +public class ExpeditionRewardDataList { + public int getHourTime() { + return hourTime; + } + public List<ExpeditionRewardData> getExpeditionRewardData() { + return expeditionRewardData; + } + + private int hourTime; + private List<ExpeditionRewardData> expeditionRewardData; +} diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardInfo.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardInfo.java new file mode 100644 index 000000000..4e0039ff9 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionRewardInfo.java @@ -0,0 +1,16 @@ +package emu.grasscutter.game.expedition; + +import java.util.List; + +public class ExpeditionRewardInfo { + public int getExpId() { + return expId; + } + + public List<ExpeditionRewardDataList> getExpeditionRewardDataList() { + return expeditionRewardDataList; + } + + private int expId; + private List<ExpeditionRewardDataList> expeditionRewardDataList; +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index b93baccf8..6b01c18f6 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -14,6 +14,7 @@ import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.expedition.ExpeditionInfo; import emu.grasscutter.game.friends.FriendsList; import emu.grasscutter.game.friends.PlayerProfile; import emu.grasscutter.game.gacha.PlayerGachaInfo; @@ -50,6 +51,7 @@ import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.DateHelper; import emu.grasscutter.utils.Position; import emu.grasscutter.utils.MessageHandler; +import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -101,6 +103,7 @@ public class Player { private ArrayList<AvatarProfileData> shownAvatars; private Set<Integer> rewardedLevels; private ArrayList<ShopLimit> shopLimit; + private Map<Long, ExpeditionInfo> expeditionInfo; private int sceneId; private int regionId; @@ -170,6 +173,7 @@ public class Player { this.moonCardGetTimes = new HashSet<>(); this.shopLimit = new ArrayList<>(); + this.expeditionInfo = new HashMap<>(); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); this.movementManager = new MovementManager(this); @@ -673,6 +677,28 @@ public class Player { session.send(new PacketCardProductRewardNotify(getMoonCardRemainDays())); } + public Map<Long, ExpeditionInfo> getExpeditionInfo() { + return expeditionInfo; + } + + public void addExpeditionInfo(long avaterGuid, int expId, int hourTime, int startTime){ + ExpeditionInfo exp = new ExpeditionInfo(); + exp.setExpId(expId); + exp.setHourTime(hourTime); + exp.setState(1); + exp.setStartTime(startTime); + expeditionInfo.put(avaterGuid, exp); + } + + public void removeExpeditionInfo(long avaterGuid){ + expeditionInfo.remove(avaterGuid); + } + + public ExpeditionInfo getExpeditionInfo(long avaterGuid){ + return expeditionInfo.get(avaterGuid); + } + + public List<ShopLimit> getShopLimit() { return shopLimit; } @@ -1029,6 +1055,22 @@ public class Player { this.resetSendPlayerLocTime(); } } + // Expedition + var timeNow = Utils.getCurrentSeconds(); + var needNotify = false; + for (Long key : expeditionInfo.keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + if(e.getState() == 1){ + if(timeNow - e.getStartTime() >= e.getHourTime() * 60 * 60){ + e.setState(2); + needNotify = true; + } + } + } + if(needNotify){ + this.save(); + this.sendPacket(new PacketAvatarExpeditionDataNotify(this)); + } } diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index db4a8c32b..3b5d91837 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -8,6 +8,7 @@ import emu.grasscutter.game.Account; import emu.grasscutter.game.combine.CombineManger; import emu.grasscutter.game.drop.DropManager; import emu.grasscutter.game.dungeons.DungeonManager; +import emu.grasscutter.game.expedition.ExpeditionManager; import emu.grasscutter.game.gacha.GachaManager; import emu.grasscutter.game.managers.ChatManager; import emu.grasscutter.game.managers.InventoryManager; @@ -42,6 +43,7 @@ public final class GameServer extends KcpServer { private final ShopManager shopManager; private final MultiplayerManager multiplayerManager; private final DungeonManager dungeonManager; + private final ExpeditionManager expeditionManager; private final CommandMap commandMap; private final TaskMap taskMap; private final DropManager dropManager; @@ -66,6 +68,7 @@ public final class GameServer extends KcpServer { this.commandMap = new CommandMap(true); this.taskMap = new TaskMap(true); this.dropManager = new DropManager(this); + this.expeditionManager = new ExpeditionManager(this); this.combineManger = new CombineManger(this); // Schedule game loop. @@ -124,7 +127,11 @@ public final class GameServer extends KcpServer { public DungeonManager getDungeonManager() { return dungeonManager; } - + + public ExpeditionManager getExpeditionManager() { + return expeditionManager; + } + public CommandMap getCommandMap() { return this.commandMap; } @@ -132,6 +139,7 @@ public final class GameServer extends KcpServer { public CombineManger getCombineManger(){ return this.combineManger; } + public TaskMap getTaskMap() { return this.taskMap; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionAllDataReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionAllDataReq.java new file mode 100644 index 000000000..f1a30ab0d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionAllDataReq.java @@ -0,0 +1,15 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionAllDataRsp; + +@Opcodes(PacketOpcodes.AvatarExpeditionAllDataReq) +public class HandlerAvatarExpeditionAllDataReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + session.send(new PacketAvatarExpeditionAllDataRsp(session.getPlayer())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionCallBackReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionCallBackReq.java new file mode 100644 index 000000000..0ac05ed0b --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionCallBackReq.java @@ -0,0 +1,25 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionCallBackReqOuterClass.AvatarExpeditionCallBackReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionCallBackRsp; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionStartRsp; +import emu.grasscutter.utils.Utils; + +@Opcodes(PacketOpcodes.AvatarExpeditionCallBackReq) +public class HandlerAvatarExpeditionCallBackReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + AvatarExpeditionCallBackReq req = AvatarExpeditionCallBackReq.parseFrom(payload); + + for (int i = 0; i < req.getAvatarGuidCount(); i++) { + session.getPlayer().removeExpeditionInfo(req.getAvatarGuid(i)); + } + + session.getPlayer().save(); + session.send(new PacketAvatarExpeditionCallBackRsp(session.getPlayer())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionGetRewardReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionGetRewardReq.java new file mode 100644 index 000000000..3e3bed9f1 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionGetRewardReq.java @@ -0,0 +1,61 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.drop.DropData; +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.expedition.ExpeditionRewardData; +import emu.grasscutter.game.expedition.ExpeditionRewardDataList; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionGetRewardReqOuterClass.AvatarExpeditionGetRewardReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionCallBackRsp; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionGetRewardRsp; +import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; +import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; +import emu.grasscutter.utils.Utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +@Opcodes(PacketOpcodes.AvatarExpeditionGetRewardReq) +public class HandlerAvatarExpeditionGetRewardReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + AvatarExpeditionGetRewardReq req = AvatarExpeditionGetRewardReq.parseFrom(payload); + + ExpeditionInfo expInfo = session.getPlayer().getExpeditionInfo(req.getAvatarGuid()); + + List<GameItem> items = new LinkedList<>(); + + if (session.getServer().getExpeditionManager().getExpeditionRewardDataList().containsKey(expInfo.getExpId())) { + for (ExpeditionRewardDataList RewardDataList : session.getServer().getExpeditionManager().getExpeditionRewardDataList().get(expInfo.getExpId())) { + if(RewardDataList.getHourTime() == expInfo.getHourTime()){ + if(!RewardDataList.getExpeditionRewardData().isEmpty()){ + for (ExpeditionRewardData RewardData :RewardDataList.getExpeditionRewardData()) { + int num = RewardData.getMinCount(); + if(RewardData.getMinCount() != RewardData.getMaxCount()){ + num = Utils.randomRange(RewardData.getMinCount(), RewardData.getMaxCount()); + } + items.add(new GameItem(RewardData.getItemId(), num)); + } + } + } + } + } + + session.getPlayer().getInventory().addItems(items); + session.getPlayer().sendPacket(new PacketItemAddHintNotify(items, ActionReason.ExpeditionReward)); + + session.getPlayer().removeExpeditionInfo(req.getAvatarGuid()); + session.getPlayer().save(); + session.send(new PacketAvatarExpeditionGetRewardRsp(session.getPlayer(), items)); + } +} + diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionStartReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionStartReq.java new file mode 100644 index 000000000..74395e30c --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarExpeditionStartReq.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionStartReqOuterClass.AvatarExpeditionStartReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarExpeditionStartRsp; +import emu.grasscutter.utils.Utils; + +@Opcodes(PacketOpcodes.AvatarExpeditionStartReq) +public class HandlerAvatarExpeditionStartReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + AvatarExpeditionStartReq req = AvatarExpeditionStartReq.parseFrom(payload); + + int startTime = Utils.getCurrentSeconds(); + session.getPlayer().addExpeditionInfo(req.getAvatarGuid(), req.getExpId(), req.getHourTime(), startTime); + session.getPlayer().save(); + session.send(new PacketAvatarExpeditionStartRsp(session.getPlayer())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionAllDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionAllDataRsp.java new file mode 100644 index 000000000..e6772f928 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionAllDataRsp.java @@ -0,0 +1,33 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionAllDataRspOuterClass.AvatarExpeditionAllDataRsp; +import emu.grasscutter.net.proto.AvatarExpeditionInfoOuterClass.AvatarExpeditionInfo; + +import java.util.*; + +public class PacketAvatarExpeditionAllDataRsp extends BasePacket { + public PacketAvatarExpeditionAllDataRsp(Player player) { + super(PacketOpcodes.AvatarExpeditionAllDataRsp); + + List<Integer> openExpeditionList = new ArrayList<>(List.of(306,305,304,303,302,301,206,105,204,104,203,103,202,101,102,201,106,205)); + Map<Long, AvatarExpeditionInfo> avatarExpeditionInfoList = new HashMap<Long, AvatarExpeditionInfo>(); + + var expeditionInfo = player.getExpeditionInfo(); + for (Long key : player.getExpeditionInfo().keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + avatarExpeditionInfoList.put(key, AvatarExpeditionInfo.newBuilder().setStateValue(e.getState()).setExpId(e.getExpId()).setHourTime(e.getHourTime()).setStartTime(e.getStartTime()).build()); + }; + + AvatarExpeditionAllDataRsp.Builder proto = AvatarExpeditionAllDataRsp.newBuilder() + .addAllOpenExpeditionList(openExpeditionList) + .setExpeditionCountLimit(5) + .putAllExpeditionInfoMap(avatarExpeditionInfoList); + + this.setData(proto.build()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionCallBackRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionCallBackRsp.java new file mode 100644 index 000000000..30c927bc9 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionCallBackRsp.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionCallBackRspOuterClass.AvatarExpeditionCallBackRsp; +import emu.grasscutter.net.proto.AvatarExpeditionInfoOuterClass.AvatarExpeditionInfo; + +public class PacketAvatarExpeditionCallBackRsp extends BasePacket { + public PacketAvatarExpeditionCallBackRsp(Player player) { + super(PacketOpcodes.AvatarExpeditionCallBackRsp); + + AvatarExpeditionCallBackRsp.Builder proto = AvatarExpeditionCallBackRsp.newBuilder(); + var expeditionInfo = player.getExpeditionInfo(); + for (Long key : player.getExpeditionInfo().keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + proto.putExpeditionInfoMap(key, AvatarExpeditionInfo.newBuilder().setStateValue(e.getState()).setExpId(e.getExpId()).setHourTime(e.getHourTime()).setStartTime(e.getStartTime()).build()); + }; + + this.setData(proto.build()); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionDataNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionDataNotify.java new file mode 100644 index 000000000..1fe6bbee0 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionDataNotify.java @@ -0,0 +1,29 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionDataNotifyOuterClass.AvatarExpeditionDataNotify; +import emu.grasscutter.net.proto.AvatarExpeditionInfoOuterClass.AvatarExpeditionInfo; + +import java.util.*; + +public class PacketAvatarExpeditionDataNotify extends BasePacket { + public PacketAvatarExpeditionDataNotify(Player player) { + super(PacketOpcodes.AvatarExpeditionDataNotify); + + Map<Long, AvatarExpeditionInfo> avatarExpeditionInfoList = new HashMap<Long, AvatarExpeditionInfo>(); + + var expeditionInfo = player.getExpeditionInfo(); + for (Long key : player.getExpeditionInfo().keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + avatarExpeditionInfoList.put(key, AvatarExpeditionInfo.newBuilder().setStateValue(e.getState()).setExpId(e.getExpId()).setHourTime(e.getHourTime()).setStartTime(e.getStartTime()).build()); + }; + + AvatarExpeditionDataNotify.Builder proto = AvatarExpeditionDataNotify.newBuilder() + .putAllExpeditionInfoMap(avatarExpeditionInfoList); + + this.setData(proto.build()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionGetRewardRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionGetRewardRsp.java new file mode 100644 index 000000000..34f0ea115 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionGetRewardRsp.java @@ -0,0 +1,30 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionGetRewardRspOuterClass.AvatarExpeditionGetRewardRsp; +import emu.grasscutter.net.proto.AvatarExpeditionInfoOuterClass.AvatarExpeditionInfo; + +import java.util.Collection; + +public class PacketAvatarExpeditionGetRewardRsp extends BasePacket { + public PacketAvatarExpeditionGetRewardRsp(Player player, Collection<GameItem> items) { + super(PacketOpcodes.AvatarExpeditionGetRewardRsp); + + AvatarExpeditionGetRewardRsp.Builder proto = AvatarExpeditionGetRewardRsp.newBuilder(); + var expeditionInfo = player.getExpeditionInfo(); + for (Long key : player.getExpeditionInfo().keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + proto.putExpeditionInfoMap(key, AvatarExpeditionInfo.newBuilder().setStateValue(e.getState()).setExpId(e.getExpId()).setHourTime(e.getHourTime()).setStartTime(e.getStartTime()).build()); + }; + + for (GameItem item : items) { + proto.addItemList(item.toItemParam()); + } + + this.setData(proto.build()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionStartRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionStartRsp.java new file mode 100644 index 000000000..9a279124d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarExpeditionStartRsp.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.expedition.ExpeditionInfo; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarExpeditionInfoOuterClass.AvatarExpeditionInfo; +import emu.grasscutter.net.proto.AvatarExpeditionStartRspOuterClass.AvatarExpeditionStartRsp; + +public class PacketAvatarExpeditionStartRsp extends BasePacket { + public PacketAvatarExpeditionStartRsp(Player player) { + super(PacketOpcodes.AvatarExpeditionStartRsp); + + AvatarExpeditionStartRsp.Builder proto = AvatarExpeditionStartRsp.newBuilder(); + var expeditionInfo = player.getExpeditionInfo(); + for (Long key : player.getExpeditionInfo().keySet()) { + ExpeditionInfo e = expeditionInfo.get(key); + proto.putExpeditionInfoMap(key, AvatarExpeditionInfo.newBuilder().setStateValue(e.getState()).setExpId(e.getExpId()).setHourTime(e.getHourTime()).setStartTime(e.getStartTime()).build()); + }; + + this.setData(proto.build()); + } +} From 2b58d6953418b285cc8a609989c5844789c992cd Mon Sep 17 00:00:00 2001 From: cfuncode <36926346+cfuncode@users.noreply.github.com> Date: Sat, 7 May 2022 04:39:37 +0800 Subject: [PATCH 134/434] Improved fault tolerance for json file deserialization (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复一个Gson发序列化json文件的空值问题 * Improved fault tolerance for json file deserialization --- .../emu/grasscutter/data/ResourceLoader.java | 16 +++--- .../java/emu/grasscutter/utils/Utils.java | 55 ++++++++++++++++--- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 5478052d8..b1e3da9ff 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -7,6 +7,7 @@ import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.gson.Gson; import emu.grasscutter.utils.Utils; import org.reflections.Reflections; @@ -120,14 +121,15 @@ public class ResourceLoader { @SuppressWarnings({"rawtypes", "unchecked"}) protected static void loadFromResource(Class<?> c, String fileName, Int2ObjectMap map) throws Exception { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().RESOURCE_FOLDER + "ExcelBinOutput/" + fileName)) { - List list = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, c).getType()); + FileReader fileReader = new FileReader(Grasscutter.getConfig().RESOURCE_FOLDER + "ExcelBinOutput/" + fileName); + Gson gson = Grasscutter.getGsonFactory(); + List list = gson.fromJson(fileReader, List.class); - for (Object o : list) { - GameResource res = (GameResource) o; - res.onLoad(); - map.put(res.getId(), res); - } + for (Object o : list) { + Map<String, Object> tempMap = Utils.switchPropertiesUpperLowerCase((Map<String, Object>) o, c); + GameResource res = gson.fromJson(gson.toJson(tempMap), TypeToken.get(c).getType()); + res.onLoad(); + map.put(res.getId(), res); } } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 1b4a00b90..931a37617 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -1,20 +1,24 @@ package emu.grasscutter.utils; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.time.*; -import java.time.temporal.TemporalAdjusters; -import java.util.Random; - import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; - import org.slf4j.Logger; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.DayOfWeek; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) public final class Utils { public static final Random random = new Random(); @@ -230,4 +234,39 @@ public final class Utils { } return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); } + + public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) { + Map<String, Object> map = new HashMap<>(objMap.size()); + for (String key : objMap.keySet()) { + try { + char c = key.charAt(0); + if (c >= 'a' && c <= 'z') { + try { + cls.getDeclaredField(key); + map.put(key, objMap.get(key)); + } catch (NoSuchFieldException e) { + String s1 = String.valueOf(c).toUpperCase(); + String after = key.length() > 1 ? s1 + key.substring(1) : s1; + cls.getDeclaredField(after); + map.put(after, objMap.get(key)); + } + } else if (c >= 'A' && c <= 'Z') { + try { + cls.getDeclaredField(key); + map.put(key, objMap.get(key)); + } catch (NoSuchFieldException e) { + String s1 = String.valueOf(c).toLowerCase(); + String after = key.length() > 1 ? s1 + key.substring(1) : s1; + cls.getDeclaredField(after); + map.put(after, objMap.get(key)); + } + } + } catch (NoSuchFieldException e) { + map.put(key, objMap.get(key)); + } + } + + return map; + } + } From c4e11088da198b69785359ffde253a4b188b86d4 Mon Sep 17 00:00:00 2001 From: Scald <104459145+Arikatsu@users.noreply.github.com> Date: Sat, 7 May 2022 02:11:29 +0530 Subject: [PATCH 135/434] Add config option to change console server's signature, namecard, level and world level (#586) * Add files via upload * Add files via upload * Update GameConstants.java * Update Config.java * Update PacketGetPlayerFriendListRsp.java --- src/main/java/emu/grasscutter/Config.java | 4 ++++ src/main/java/emu/grasscutter/GameConstants.java | 4 ++++ .../server/packet/send/PacketGetPlayerFriendListRsp.java | 8 ++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index c911a1cf0..f31d3b7cc 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -82,6 +82,10 @@ public final class Config { public boolean WatchGacha = false; public String ServerNickname = "Server"; public int ServerAvatarId = 10000007; + public int ServerNameCardId = 210001; + public int ServerLevel = 1; + public int ServerWorldLevel = 1; + public String ServerSignature = "Server Signature"; public int[] WelcomeEmotes = {2007, 1002, 4010}; public String WelcomeMotd = "Welcome to Grasscutter emu"; public String WelcomeMailTitle = "Welcome to Grasscutter!"; diff --git a/src/main/java/emu/grasscutter/GameConstants.java b/src/main/java/emu/grasscutter/GameConstants.java index 39b28b736..4bebf9c81 100644 --- a/src/main/java/emu/grasscutter/GameConstants.java +++ b/src/main/java/emu/grasscutter/GameConstants.java @@ -13,7 +13,11 @@ public final class GameConstants { public static final int MAIN_CHARACTER_FEMALE = 10000007; public static final String SERVER_AVATAR_NAME = Grasscutter.getConfig().getGameServerOptions().ServerNickname; public static final int SERVER_AVATAR_ID = Grasscutter.getConfig().getGameServerOptions().ServerAvatarId; + public static final String SERVER_SIGNATURE = Grasscutter.getConfig().getGameServerOptions().ServerSignature; public static final Position START_POSITION = new Position(2747, 194, -1719); + public static final int SERVER_NAMECARD_ID = Grasscutter.getConfig().getGameServerOptions().ServerNameCardId; + public static final int SERVER_LEVEL = Grasscutter.getConfig().getGameServerOptions().ServerLevel; + public static final int SERVER_WORLD_LEVEL = Grasscutter.getConfig().getGameServerOptions().ServerWorldLevel; public static final int MAX_FRIENDS = 45; public static final int MAX_FRIEND_REQUESTS = 50; 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 d7a9427b8..781faf411 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java @@ -19,12 +19,12 @@ public class PacketGetPlayerFriendListRsp extends BasePacket { FriendBrief serverFriend = FriendBrief.newBuilder() .setUid(GameConstants.SERVER_CONSOLE_UID) .setNickname(GameConstants.SERVER_AVATAR_NAME) - .setLevel(1) + .setLevel(GameConstants.SERVER_LEVEL) .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(GameConstants.SERVER_AVATAR_ID)) - .setWorldLevel(0) - .setSignature("") + .setWorldLevel(GameConstants.SERVER_WORLD_LEVEL) + .setSignature(GameConstants.SERVER_SIGNATURE) .setLastActiveTime((int) (System.currentTimeMillis() / 1000f)) - .setNameCardId(210001) + .setNameCardId(GameConstants.SERVER_NAMECARD_ID) .setOnlineState(FriendOnlineState.FRIEND_ONLINE) .setParam(1) .setIsGameSource(true) From 71095786b851a7bd86657d775d2fd3af8c6caf56 Mon Sep 17 00:00:00 2001 From: wulf <lilch1022@hotmail.com> Date: Fri, 6 May 2022 23:21:23 +0800 Subject: [PATCH 136/434] fix scence block loading bug --- src/main/java/emu/grasscutter/scripts/SceneScriptManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 1784c827b..5f6a1b7e6 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -166,7 +166,7 @@ public class SceneScriptManager { List<SceneBlock> blocks = ScriptLoader.getSerializer().toList(SceneBlock.class, bindings.get("block_rects")); for (int i = 0; i < blocks.size(); i++) { - SceneBlock block = blocks.get(0); + SceneBlock block = blocks.get(i); block.id = blockIds.get(i); loadBlockFromScript(block); From 1d4a41fd61f6eb2315b6e69ce5df4fe05be4aeb6 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 14:38:14 -0700 Subject: [PATCH 137/434] Allow walking away from the statue within 2.5s to stop auto heal. --- .../managers/SotSManager/SotSManager.java | 82 +++++++++++++------ .../HandlerExitTransPointRegionNotify.java | 18 ++++ 2 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java index 4edad231e..0bfdf9454 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java @@ -2,9 +2,13 @@ package emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.managers.MovementManager.MovementManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.world.World; import emu.grasscutter.net.proto.ChangeHpReasonOuterClass; import emu.grasscutter.net.proto.PropChangeReasonOuterClass; import emu.grasscutter.server.game.GameSession; @@ -14,6 +18,8 @@ import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotif import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; // Statue of the Seven Manager public class SotSManager { @@ -21,6 +27,7 @@ public class SotSManager { // NOTE: Spring volume balance *1 = fight prop HP *100 private final Player player; + private Timer autoRecoverTimer; public SotSManager(Player player) { this.player = player; @@ -46,26 +53,44 @@ public class SotSManager { public void autoRevive(GameSession session) { player.getTeamManager().getActiveTeam().forEach(entity -> { boolean isAlive = entity.isAlive(); + float currentHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); +// Grasscutter.getLogger().debug("" + entity.getAvatar().getAvatarData().getName() + "\t" + currentHP + "/" + maxHP + "\t" + (isAlive ? "ALIVE":"DEAD")); + float newHP = (float)(maxHP * 0.3); + if (currentHP < newHP) { + updateAvatarCurHP(session, entity, newHP); + } if (!isAlive) { - float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float newHP = (float)(maxHP * 0.3); - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); } }); } public void scheduleAutoRecover(GameSession session) { - // TODO: play audio effects? possibly client side? - client automatically plays. - // delay 2.5 seconds - new Thread(() -> { - try { - Thread.sleep(2500); - autoRecover(session); - } catch (Exception e) { - Grasscutter.getLogger().error(e.getMessage()); - } - }).start(); + if (autoRecoverTimer == null) { + autoRecoverTimer = new Timer(); + autoRecoverTimer.schedule(new AutoRecoverTimerTick(session), 2500); + } + } + + public void cancelAutoRecover() { + if (autoRecoverTimer != null) { + autoRecoverTimer.cancel(); + autoRecoverTimer = null; + } + } + + private class AutoRecoverTimerTick extends TimerTask + { + private GameSession session; + + public AutoRecoverTimerTick(GameSession session) { + this.session = session; + } + public void run() { + autoRecover(session); + cancelAutoRecover(); + } } public void refillSpringVolume() { @@ -124,24 +149,27 @@ public class SotSManager { float newHP = currentHP + needSV / 100; // convert SV to HP - // TODO: Figure out why client shows current HP instead of added HP. - // Say an avatar had 12000 and now has 14000, it should show "2000". - // The client always show "+14000" which is incorrect. - - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); - session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP, - newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER, - ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue)); - session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); - - Avatar avatar = entity.getAvatar(); - avatar.setCurrentHp(newHP); - session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP)); - player.save(); + updateAvatarCurHP(session, entity, newHP); } }); } } + private void updateAvatarCurHP(GameSession session, EntityAvatar entity, float newHP) { + // TODO: Figure out why client shows current HP instead of added HP. + // Say an avatar had 12000 and now has 14000, it should show "2000". + // The client always show "+14000" which is incorrect. + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP, + newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER, + ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue)); + session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + + Avatar avatar = entity.getAvatar(); + avatar.setCurrentHp(newHP); + session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP)); + player.save(); + } + } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java new file mode 100644 index 000000000..35ec957cb --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.managers.SotSManager.SotSManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.ExitTransPointRegionNotify) +public class HandlerExitTransPointRegionNotify extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception{ + Player player = session.getPlayer(); + SotSManager sotsManager = player.getSotSManager(); + sotsManager.cancelAutoRecover(); + } +} From c582814bc1d87fe2915bda210947e8798bd21113 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 15:10:20 -0700 Subject: [PATCH 138/434] Fix #593: Accidental death of character --- .../managers/MovementManager/MovementManager.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index a08280378..ed1645936 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -60,6 +60,7 @@ public class MovementManager { private final Player player; private float landSpeed = 0; + private long landTimeMillisecond = 0; private Timer movementManagerTickTimer; private GameSession cachedSession = null; private GameEntity cachedEntity = null; @@ -192,12 +193,20 @@ public class MovementManager { // cache land speed if (state == MotionState.MOTION_LAND_SPEED) { landSpeed = motionInfo.getSpeed().getY(); + landTimeMillisecond = System.currentTimeMillis(); } if (state == MotionState.MOTION_FALL_ON_GROUND) { + // if not received immediately after MOTION_LAND_SPEED, discard this packet. + // TODO: Test in high latency. + int maxDelay = 200; + if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) { + Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard."); + return; + } float currentHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); float maxHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); float damage = 0; -// Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); + Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); if (landSpeed < -23.5) { damage = (float)(maxHP * 0.33); } @@ -214,7 +223,7 @@ public class MovementManager { if (newHP < 0) { newHP = 0; } -// Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); + Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); if (newHP == 0) { From 438f845e3a3b44c7011e384a716d69ac5fd05fa5 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 18:50:18 -0400 Subject: [PATCH 139/434] Convert to the superior language system. (pt. 3) --- .../command/commands/CoopCommand.java | 15 ++++++------- .../command/commands/GiveAllCommand.java | 10 +++++---- .../command/commands/GiveArtifactCommand.java | 16 +++++++------- .../commands/KillCharacterCommand.java | 5 +++-- .../command/commands/ListCommand.java | 4 +++- .../command/commands/PermissionCommand.java | 14 ++++++------- .../commands/ResetShopLimitCommand.java | 4 +++- .../command/commands/SendMessageCommand.java | 5 ++--- .../commands/SetFetterLevelCommand.java | 11 +++++----- .../command/commands/SetStatsCommand.java | 1 - .../commands/SetWorldLevelCommand.java | 10 +++++---- .../command/commands/SpawnCommand.java | 14 +++++++------ .../command/commands/StopCommand.java | 8 ++++--- .../command/commands/TeleportAllCommand.java | 6 +++++- .../command/commands/TeleportCommand.java | 15 ++++++++----- .../command/commands/WeatherCommand.java | 11 +++++----- .../DefaultAuthenticationHandler.java | 21 +++++++++---------- 17 files changed, 97 insertions(+), 73 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index d41805c82..96411019b 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "coop", usage = "coop [host UID]", description = "Forces someone to join the world of others", permission = "server.coop") public final class CoopCommand implements CommandHandler { @@ -20,26 +22,25 @@ public final class CoopCommand implements CommandHandler { Player host = sender; switch (args.size()) { case 0: // Summon target to self - if (sender == null) { // Console doesn't have a self to summon to - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Coop_usage); + CommandHandler.sendMessage(sender, translate("commands.coop.usage")); + if (sender == null) // Console doesn't have a self to summon to return; - } break; case 1: // Summon target to argument try { int hostId = Integer.parseInt(args.get(0)); host = Grasscutter.getGameServer().getPlayerByUid(hostId); if (host == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Player_is_offline); + CommandHandler.sendMessage(sender, translate("commands.execution.player_offline_error")); return; } break; } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_playerId); + CommandHandler.sendMessage(sender, translate("commands.execution.uid_error")); return; } default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Coop_usage); + CommandHandler.sendMessage(sender, translate("commands.coop.usage")); return; } @@ -49,6 +50,6 @@ public final class CoopCommand implements CommandHandler { } host.getServer().getMultiplayerManager().applyEnterMp(targetPlayer, host.getUid()); targetPlayer.getServer().getMultiplayerManager().applyEnterMpReply(host, targetPlayer.getUid(), true); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Coop_success.replace("{host}", host.getNickname()).replace("{target}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.coop.success", targetPlayer.getNickname(), host.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index ea249af9e..bb11de3c2 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -13,6 +13,8 @@ import emu.grasscutter.game.player.Player; import java.util.*; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "giveall", usage = "giveall [amount]", description = "Gives all items", aliases = {"givea"}, permission = "player.giveall", threading = true) public final class GiveAllCommand implements CommandHandler { @@ -32,21 +34,21 @@ public final class GiveAllCommand implements CommandHandler { try { amount = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_amount); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); return; } break; default: // invalid - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveAll_usage); + CommandHandler.sendMessage(sender, translate("commands.giveAll.usage")); return; } this.giveAllItems(targetPlayer, amount); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveAll_done); + CommandHandler.sendMessage(sender, translate("commands.giveAll.success", targetPlayer.getNickname())); } public void giveAllItems(Player player, int amount) { - CommandHandler.sendMessage(player, Grasscutter.getLanguage().GiveAll_item); + CommandHandler.sendMessage(player, translate("commands.giveAll.started")); for (AvatarData avatarData: GameData.getAvatarDataMap().values()) { //Exclude test avatar diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 5b8351a5a..541cc440e 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -14,6 +14,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", description = "Gives the player a specified artifact", aliases = {"gart"}, permission = "player.giveart") public final class GiveArtifactCommand implements CommandHandler { @Override @@ -24,7 +26,7 @@ public final class GiveArtifactCommand implements CommandHandler { } if (args.size() < 2) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveArtifact_usage); + CommandHandler.sendMessage(sender, translate("commands.giveArtifact.usage")); return; } @@ -32,12 +34,12 @@ public final class GiveArtifactCommand implements CommandHandler { try { itemId = Integer.parseInt(args.remove(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_artifact_id); + CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); return; } ItemData itemData = GameData.getItemDataMap().get(itemId); if (itemData.getItemType() != ItemType.ITEM_RELIQUARY) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_artifact_id); + CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); return; } @@ -45,7 +47,7 @@ public final class GiveArtifactCommand implements CommandHandler { try { mainPropId = Integer.parseInt(args.remove(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.generic.execution.argument_error")); return; } @@ -59,7 +61,7 @@ public final class GiveArtifactCommand implements CommandHandler { } catch (NumberFormatException ignored) { // Could be a stat,times string so no need to panic } - ArrayList<Integer> appendPropIdList = new ArrayList<>(); + List<Integer> appendPropIdList = new ArrayList<>(); try { args.forEach(it -> { String[] arr; @@ -74,7 +76,7 @@ public final class GiveArtifactCommand implements CommandHandler { appendPropIdList.addAll(Collections.nCopies(n, Integer.parseInt(it))); }); } catch (Exception ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); return; } @@ -85,7 +87,7 @@ public final class GiveArtifactCommand implements CommandHandler { item.getAppendPropIdList().addAll(appendPropIdList); targetPlayer.getInventory().addItem(item, ActionReason.SubfieldDrop); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().GiveArtifact_given.replace("{itemId}", Integer.toString(itemId)).replace("target", Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate("commands.giveArtifact.success", Integer.toString(itemId), Integer.toString(targetPlayer.getUid()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 82a18f72d..f1e0f0f8c 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.entity.EntityAvatar; @@ -12,6 +11,8 @@ import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, description = "Kills the players current character", permission = "player.killcharacter") public final class KillCharacterCommand implements CommandHandler { @@ -32,6 +33,6 @@ public final class KillCharacterCommand implements CommandHandler { targetPlayer.getScene().removeEntity(entity); entity.onDeath(0); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().KillCharacter_kill_current_character.replace("{name}", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate("commands.killCharacter.success", targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ListCommand.java b/src/main/java/emu/grasscutter/command/commands/ListCommand.java index 3119f728f..bc35e65e1 100644 --- a/src/main/java/emu/grasscutter/command/commands/ListCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ListCommand.java @@ -8,6 +8,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; import java.util.Map; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "list", usage = "list [uid]", description = "List online players", aliases = {"players"}) public final class ListCommand implements CommandHandler { @@ -21,7 +23,7 @@ public final class ListCommand implements CommandHandler { needUID = args.get(0).equals("uid"); } - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().List_message.replace("{size}", Integer.toString(playersMap.size()))); + CommandHandler.sendMessage(sender, translate("commands.list.success", Integer.toString(playersMap.size()))); if (playersMap.size() != 0) { StringBuilder playerSet = new StringBuilder(); diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java index aa99939f6..69c8ce899 100644 --- a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -22,7 +22,7 @@ public final class PermissionCommand implements CommandHandler { } if (args.size() != 2) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_usage); + CommandHandler.sendMessage(sender, translate("commands.permission.usage")); return; } @@ -31,23 +31,23 @@ public final class PermissionCommand implements CommandHandler { Account account = targetPlayer.getAccount(); if (account == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Account_not_find); + CommandHandler.sendMessage(sender, translate("commands.permission.account_error")); return; } switch (action) { default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_usage); + CommandHandler.sendMessage(sender, translate("commands.permission.usage")); break; case "add": if (account.addPermission(permission)) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_add); - } else CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_have_permission); + CommandHandler.sendMessage(sender, translate("commands.permission.add")); + } else CommandHandler.sendMessage(sender, translate("commands.permission.has_error")); break; case "remove": if (account.removePermission(permission)) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_remove); - } else CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Permission_not_have_permission); + CommandHandler.sendMessage(sender, translate("commands.permission.remove")); + } else CommandHandler.sendMessage(sender, translate("commands.permission.not_have_error")); break; } diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index 59d8b32e8..aeae0abbf 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "resetshop", usage = "resetshop", description = "Reset target player's shop refresh time.", permission = "server.resetshop") public final class ResetShopLimitCommand implements CommandHandler { @@ -19,6 +21,6 @@ public final class ResetShopLimitCommand implements CommandHandler { targetPlayer.getShopLimit().forEach(x -> x.setNextRefreshTime(0)); targetPlayer.save(); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Success); + CommandHandler.sendMessage(sender, translate("commands.status.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index 41f76ba80..acf63dea0 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; @@ -20,12 +19,12 @@ public final class SendMessageCommand implements CommandHandler { return; } if (args.size() == 0) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().SendMessage_usage); + CommandHandler.sendMessage(null, translate("commands.sendMessage.usage")); return; } String message = String.join(" ", args); CommandHandler.sendMessage(targetPlayer, message); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SenaMessage_message_sent); + CommandHandler.sendMessage(sender, translate("commands.sendMessage.success")); } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index 21507b998..7184c679c 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -2,7 +2,6 @@ package emu.grasscutter.command.commands; import java.util.List; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.data.GameData; @@ -10,6 +9,8 @@ import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.player.Player; import emu.grasscutter.server.packet.send.PacketAvatarFetterDataNotify; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "setfetterlevel", usage = "setfetterlevel <level>", description = "Sets your fetter level for your current active character", aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel") @@ -23,14 +24,14 @@ public final class SetFetterLevelCommand implements CommandHandler { } if (args.size() != 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetFetterLevel_usage); + CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.usage")); return; } try { int fetterLevel = Integer.parseInt(args.get(0)); if (fetterLevel < 0 || fetterLevel > 10) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetFetterLevel_fetter_level_must_between_0_and_10); + CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.range_error")); return; } Avatar avatar = targetPlayer.getTeamManager().getCurrentAvatarEntity().getAvatar(); @@ -42,9 +43,9 @@ public final class SetFetterLevelCommand implements CommandHandler { avatar.save(); targetPlayer.sendPacket(new PacketAvatarFetterDataNotify(avatar)); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetFetterLevel_fetter_set_level.replace("{level}", Integer.toString(fetterLevel))); + CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.success", fetterLevel)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetFetterLevel_invalid_fetter_level); + CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.level_error")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index 3498e267b..233eb4d73 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -33,7 +33,6 @@ public final class SetStatsCommand implements CommandHandler { Map<String, Stat> stats = new HashMap<>(); public SetStatsCommand() { - Language lang = Grasscutter.getLanguage(); // Default stats stats.put("maxhp", new Stat(FightProperty.FIGHT_PROP_MAX_HP.toString(), FightProperty.FIGHT_PROP_MAX_HP, false)); stats.put("hp", new Stat(FightProperty.FIGHT_PROP_CUR_HP.toString(), FightProperty.FIGHT_PROP_CUR_HP, false)); diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index 39f4753a7..914d8cecc 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -7,6 +7,8 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "setworldlevel", usage = "setworldlevel <level>", description = "Sets your world level (Relog to see proper effects)", aliases = {"setworldlvl"}, permission = "player.setworldlevel") @@ -20,14 +22,14 @@ public final class SetWorldLevelCommand implements CommandHandler { } if (args.size() < 1) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetWorldLevel_usage); + CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.usage")); return; } try { int level = Integer.parseInt(args.get(0)); if (level > 8 || level < 0) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetWorldLevel_world_level_must_between_0_and_8); + CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.value_error")); return; } @@ -35,9 +37,9 @@ public final class SetWorldLevelCommand implements CommandHandler { targetPlayer.getWorld().setWorldLevel(level); targetPlayer.setWorldLevel(level); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().SetWorldLevel_set_world_level.replace("{level}", Integer.toString(level))); + CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.success", Integer.toString(level))); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().SetWorldLevel_invalid_world_level); + CommandHandler.sendMessage(null, translate("commands.setWorldLevel.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 index c50eb972d..c66a45b50 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -20,6 +20,8 @@ import javax.swing.text.html.parser.Entity; import java.util.List; import java.util.Random; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", description = "Spawns an entity near you", permission = "server.spawn") public final class SpawnCommand implements CommandHandler { @@ -39,23 +41,23 @@ public final class SpawnCommand implements CommandHandler { try { level = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); } // Fallthrough case 2: try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_amount); + CommandHandler.sendMessage(sender, translate("commands.generic.error.amount")); } // Fallthrough case 1: try { id = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_entity_id); + CommandHandler.sendMessage(sender, translate("commands.generic.error.entityId")); } break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Spawn_usage); + CommandHandler.sendMessage(sender, translate("commands.spawn.usage")); return; } @@ -63,7 +65,7 @@ public final class SpawnCommand implements CommandHandler { GadgetData gadgetData = GameData.getGadgetDataMap().get(id); ItemData itemData = GameData.getItemDataMap().get(id); if (monsterData == null && gadgetData == null && itemData == null) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_entity_id); + CommandHandler.sendMessage(sender, translate("commands.generic.error.entityId")); return; } Scene scene = targetPlayer.getScene(); @@ -99,7 +101,7 @@ public final class SpawnCommand implements CommandHandler { scene.addEntity(entity); } - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Spawn_message.replace("{amount}", Integer.toString(amount)).replace("{id}", Integer.toString(id))); + CommandHandler.sendMessage(sender, translate("commands.spawn.success", Integer.toString(amount), Integer.toString(id))); } private Position GetRandomPositionInCircle(Position origin, double radius){ diff --git a/src/main/java/emu/grasscutter/command/commands/StopCommand.java b/src/main/java/emu/grasscutter/command/commands/StopCommand.java index 4d0f8c1c5..ad4903107 100644 --- a/src/main/java/emu/grasscutter/command/commands/StopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/StopCommand.java @@ -7,17 +7,19 @@ import emu.grasscutter.game.player.Player; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "stop", usage = "stop", description = "Stops the server", permission = "server.stop") public final class StopCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - CommandHandler.sendMessage(null, Grasscutter.getLanguage().Stop_message); + CommandHandler.sendMessage(null, translate("commands.stop.success")); for (Player p : Grasscutter.getGameServer().getPlayers().values()) { - CommandHandler.sendMessage(p, Grasscutter.getLanguage().Stop_message); + CommandHandler.sendMessage(p, translate("commands.stop.success")); } - System.exit(1); + System.exit(1000); } } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index fe9b5dc49..54c6101f7 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -8,6 +8,8 @@ import emu.grasscutter.utils.Position; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "tpall", usage = "tpall", description = "Teleports all players in your world to your position", permission = "player.tpall") public final class TeleportAllCommand implements CommandHandler { @@ -19,7 +21,7 @@ public final class TeleportAllCommand implements CommandHandler { } if (!targetPlayer.getWorld().isMultiplayer()) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().TeleportAll_message); + CommandHandler.sendMessage(sender, translate("commands.teleportAll.error")); return; } @@ -30,5 +32,7 @@ public final class TeleportAllCommand implements CommandHandler { player.getWorld().transferPlayerToScene(player, targetPlayer.getSceneId(), pos); } + + CommandHandler.sendMessage(sender, translate("commands.teleportAll.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index ef7e68ae9..06b669a17 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -8,6 +8,8 @@ import emu.grasscutter.utils.Position; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, description = "Change the player's position.", permission = "player.teleport") public final class TeleportCommand implements CommandHandler { @@ -41,7 +43,7 @@ public final class TeleportCommand implements CommandHandler { try { sceneId = Integer.parseInt(args.get(3)); }catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Invalid_arguments); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); } // Fallthrough case 3: try { @@ -49,20 +51,23 @@ public final class TeleportCommand implements CommandHandler { y = parseRelative(args.get(1), y); z = parseRelative(args.get(2), z); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Teleport_invalid_position); + CommandHandler.sendMessage(sender, translate("commands.teleport.invalid_position")); } break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Teleport_usage); + CommandHandler.sendMessage(sender, translate("commands.teleport.usage")); return; } Position target_pos = new Position(x, y, z); boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, target_pos); if (!result) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Teleport_invalid_position); + CommandHandler.sendMessage(sender, translate("commands.teleport.invalid_position")); } else { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Teleport_message.replace("{name}", targetPlayer.getNickname()).replace("{x}", Float.toString(x)).replace("{y}", Float.toString(y)).replace("{z}", Float.toString(z)).replace("{id}", Integer.toString(sceneId))); + CommandHandler.sendMessage(sender, translate("commands.teleport.success", + targetPlayer.getNickname(), Float.toString(x), Float.toString(y), + Float.toString(z), Integer.toString(sceneId)) + ); } } diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index 431e53853..df8a6a01f 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -9,6 +9,8 @@ import emu.grasscutter.server.packet.send.PacketSceneAreaWeatherNotify; import java.util.List; +import static emu.grasscutter.utils.Language.translate; + @Command(label = "weather", usage = "weather <weatherId> [climateId]", description = "Changes the weather.", aliases = {"w"}, permission = "player.weather") public final class WeatherCommand implements CommandHandler { @@ -27,17 +29,17 @@ public final class WeatherCommand implements CommandHandler { try { climateId = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Weather_invalid_id); + CommandHandler.sendMessage(sender, translate("commands.weather.invalid_id")); } case 1: try { weatherId = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Weather_invalid_id); + CommandHandler.sendMessage(sender, translate("commands.weather.invalid_id")); } break; default: - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Weather_usage); + CommandHandler.sendMessage(sender, translate("commands.weather.usage")); return; } @@ -46,7 +48,6 @@ public final class WeatherCommand implements CommandHandler { targetPlayer.getScene().setWeather(weatherId); targetPlayer.getScene().setClimate(climate); targetPlayer.getScene().broadcastPacket(new PacketSceneAreaWeatherNotify(targetPlayer)); - CommandHandler.sendMessage(sender, Grasscutter.getLanguage().Weather_message.replace("{weatherId}", Integer.toString(weatherId)).replace("{climateId}", Integer.toString(climateId))); - + CommandHandler.sendMessage(sender, translate("commands.weather.success", Integer.toString(weatherId), Integer.toString(climateId))); } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java index 73c696502..e5a4ca055 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java @@ -8,6 +8,8 @@ import emu.grasscutter.server.dispatch.json.LoginResultJson; import express.http.Request; import express.http.Response; +import static emu.grasscutter.utils.Language.translate; + public class DefaultAuthenticationHandler implements AuthenticationHandler { @Override @@ -34,11 +36,9 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { // Check if account exists, else create a new one. if (account == null) { - // Account doesnt exist, so we can either auto create it if the config value is - // set + // Account doesn't 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. + // This account has been created AUTOMATICALLY. There will be no permissions added. account = DatabaseHelper.createAccountWithId(requestData.account, 0); for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { @@ -51,19 +51,18 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_login_account_create.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", req.ip(), responseData.data.account.uid)); } else { responseData.retcode = -201; - responseData.message = Grasscutter.getLanguage().Username_not_found_create_failed; + responseData.message = translate("messages.dispatch.account.username_create_error"); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_failed_login_account_no_found.replace("{ip}", req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", req.ip())); } } else { responseData.retcode = -201; - responseData.message = Grasscutter.getLanguage().Username_not_found; + responseData.message = translate("messages.dispatch.account.username_error"); - Grasscutter.getLogger().info(String - .format(Grasscutter.getLanguage().Client_failed_login_account_no_found, req.ip())); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", req.ip())); } } else { // Account was found, log the player in @@ -72,7 +71,7 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { responseData.data.account.token = account.generateSessionKey(); responseData.data.account.email = account.getEmail(); - Grasscutter.getLogger().info(Grasscutter.getLanguage().Client_login.replace("{ip}", req.ip()).replace("{uid}", responseData.data.account.uid)); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", req.ip(), responseData.data.account.uid)); } return responseData; From 7899a6069bb3e3ebcaeaba09628e51cd7a2fe708 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 18:50:43 -0400 Subject: [PATCH 140/434] Add the `en-US` language. --- src/main/resources/languages/en_US.json | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/main/resources/languages/en_US.json b/src/main/resources/languages/en_US.json index f2f6b59ec..48ffb5b0b 100644 --- a/src/main/resources/languages/en_US.json +++ b/src/main/resources/languages/en_US.json @@ -118,7 +118,8 @@ "everything": "Cleared everything for %s." }, "coop": { - "usage": "Usage: coop <playerId> <target playerId>" + "usage": "Usage: coop <playerId> <target playerId>", + "success": "Summoned %s to %s's world." }, "enter_dungeon": { "usage": "Usage: enterdungeon <dungeon id>", @@ -128,14 +129,14 @@ }, "giveAll": { "usage": "Usage: giveall [player] [amount]", - "item": "Giving all items...", - "done": "Giving all items done", + "started": "Receiving all items...", + "success": "Successfully gave all items to %s.", "invalid_amount_or_playerId": "Invalid amount or player ID." }, "giveArtifact": { "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", - "invalid_artifact_id": "Invalid artifact ID.", - "given": "Given %s to %s." + "id_error": "Invalid artifact ID.", + "success": "Given %s to %s." }, "giveChar": { "usage": "Usage: givechar <player> <itemId|itemName> [amount]", @@ -169,17 +170,18 @@ }, "killCharacter": { "usage": "Usage: /killcharacter [playerId]", - "current_character": "Killed %s current character." + "success": "Killed %s's current character." }, "list": { - "message": "There are %s player(s) online:" + "success": "There are %s player(s) online:" }, "permission": { "usage": "Usage: permission <add|remove> <username> <permission>", "add": "Permission added.", - "have_permission": "They already have this permission!", + "has_error": "They already have this permission!", "remove": "Permission removed.", - "not_have_permission": "They don't have this permission!" + "not_have_error": "They don't have this permission!", + "account_error": "The account cannot be found." }, "position": { "success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" @@ -219,13 +221,13 @@ }, "sendMessage": { "usage": "Usage: sendmessage <player> <message>", - "message_sent": "Message sent." + "success": "Message sent." }, "setFetterLevel": { "usage": "Usage: setfetterlevel <level>", - "fetter_level_must_between_0_and_10": "Fetter level must be between 0 and 10.", - "fetter_set_level": "Fetter level set to %s", - "invalid_fetter_level": "Invalid fetter level." + "range_error": "Fetter level must be between 0 and 10.", + "success": "Fetter level set to %s", + "level_error": "Invalid fetter level." }, "setStats": { "usage_console": "Usage: setstats|stats @<UID> <stat> <value>", @@ -240,16 +242,16 @@ }, "setWorldLevel": { "usage": "Usage: setworldlevel <level>", - "world_level_must_between_0_and_8": "World level must be between 0-8", - "set_world_level": "World level set to %s.", + "value_error": "World level must be between 0-8", + "success": "World level set to %s.", "invalid_world_level": "Invalid world level." }, "spawn": { "usage": "Usage: spawn <entityId> [amount] [level(monster only)]", - "message": "Spawned %s of %s." + "success": "Spawned %s of %s." }, "stop": { - "message": "Server shutting down..." + "success": "Server shutting down..." }, "talent": { "usage_1": "To set talent level: /talent set <talentID> <value>", @@ -268,18 +270,19 @@ "q_skill_id": "Q skill ID %s." }, "teleportAll": { - "message": "You only can use this command in MP mode." + "success": "Summoned all players to your location.", + "error": "You only can use this command in MP mode." }, "teleport": { "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", "specify_player_id": "You must specify a player id.", "invalid_position": "Invalid position.", - "message": "Teleported %s to %s,%s,%s in scene %s" + "success": "Teleported %s to %s, %s, %s in scene %s" }, "weather": { "usage": "Usage: weather <weatherId> [climateId]", - "message": "Changed weather to %s with climate %s", + "success": "Changed weather to %s with climate %s", "invalid_id": "Invalid ID." }, "drop": { From 57a3d535a7d87002da83014392c9f995cfdcd97d Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 18:56:19 -0400 Subject: [PATCH 141/434] Add the `zh-TW` language. --- Grasscutter.iml | 865 ++++++++++++++++++++++++ Grasscutter.ipr | 104 +++ Grasscutter.iws | 207 ++++++ grasscutter-1.0.3-dev.jar.asc | 14 + src/main/resources/languages/zh-TW.json | 298 ++++++++ 5 files changed, 1488 insertions(+) create mode 100644 Grasscutter.iml create mode 100644 Grasscutter.ipr create mode 100644 Grasscutter.iws create mode 100644 grasscutter-1.0.3-dev.jar.asc create mode 100644 src/main/resources/languages/zh-TW.json diff --git a/Grasscutter.iml b/Grasscutter.iml new file mode 100644 index 000000000..0f136c501 --- /dev/null +++ b/Grasscutter.iml @@ -0,0 +1,865 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module relativePaths="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output/> + <orderEntry type="inheritedJdk"/> + <content url="file://$MODULE_DIR$/"> + <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false"/> + <sourceFolder url="file://$MODULE_DIR$/proto" isTestSource="false"/> + <sourceFolder url="file://$MODULE_DIR$/src/main/proto" isTestSource="false"/> + <sourceFolder url="file://$MODULE_DIR$/src/generated" isTestSource="false"/> + <sourceFolder url="file://$MODULE_DIR$/build/extracted-include-protos/main" isTestSource="false" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/build/extracted-protos/main" isTestSource="false" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/src/generated/main/java" isTestSource="false" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/src/test/proto" isTestSource="true"/> + <sourceFolder url="file://$MODULE_DIR$/build/extracted-include-protos/test" isTestSource="true" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/build/extracted-protos/test" isTestSource="true" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/src/generated/test/java" isTestSource="true" generated="true"/> + <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource"/> + <excludeFolder url="file://$MODULE_DIR$/.gradle"/> + <excludeFolder url="file://$MODULE_DIR$/build"/> + </content> + <orderEntry type="sourceFolder" forTests="false"/> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.9/7d495522b08a9a66084bf417e70eedf95ef706bc/logback-classic-1.2.9.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.9/e62ec78303270aefa40721154dc591b9c86072b8/logback-classic-1.2.9-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.reflections/reflections/0.10.2/b638d7ca0e0fe0146b60a0e7ba232ad852a73b31/reflections-0.10.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.reflections/reflections/0.10.2/68391d7abbca924e397401333c5a9492531812b6/reflections-0.10.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/dev.morphia.morphia/morphia-core/2.2.6/8dd21dd7e49cc78d8e726e236bdcd28510ad776a/morphia-core-2.2.6.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/dev.morphia.morphia/morphia-core/2.2.6/7364d3ec3ff8083a6c8263e26c8459ca9908278f/morphia-core-2.2.6-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz/2.3.2/18a6d6b5a40b77bd060b34cb9f2acadc4bae7c8a/quartz-2.3.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz/2.3.2/c22b720ab9a92367424cda3d628fea758dd7e4cf/quartz-2.3.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP-java7/2.4.13/3e441eddedb374d4de8e3abbb0c90997f51cc97b/HikariCP-java7-2.4.13.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP-java7/2.4.13/fc995c9e7945c5d9efa11fdbb5cbf10606409cd5/HikariCP-java7-2.4.13-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/ae9c1aae0033af915cfa75d850eb9d880f21a701/slf4j-api-1.7.36-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.9/cdaca0cf922c5791a8efa0063ec714ca974affe3/logback-core-1.2.9.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.9/92412f3c48649ab2dbe036dd9a57af0a741cb27e/logback-core-1.2.9-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-all/4.1.71.Final/8c89f16505b815e966bb1d4bf4681bdd3701b8b1/netty-all-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.8/431fc3cbc0ff81abdbfde070062741089c3ba874/gson-2.8.8.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.8/c0f02b42d0474823279fc9606a81338896d59941/gson-2.8.8-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.18.2/17e444501d7ed8dd1f6348f5bc0ad627200defa8/protobuf-java-3.18.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.18.2/2f741dd5fdc324ed91ac2916573a199302397d75/protobuf-java-3.18.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.greenrobot/eventbus-java/3.3.1/74487b0caceca6fcd1aff45d41a9cdc6910d7f5a/eventbus-java-3.3.1.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.greenrobot/eventbus-java/3.3.1/d59e3be28ebbbc065159aabfa9e066edb584baf2/eventbus-java-3.3.1-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.danilopianini/java-quadtree/0.1.9/3eb9cde063327dfefa62e281f7858b44d6d90006/java-quadtree-0.1.9.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.danilopianini/java-quadtree/0.1.9/9a127c827d012257fb39607dc9c56a911e6de482/java-quadtree-0.1.9-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz-jobs/2.3.2/b2223bd809ffc77d41a2739fde85b822e59be2fe/quartz-jobs-2.3.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz-jobs/2.3.2/26358986162924db92d4205754c37d213065f388/quartz-jobs-2.3.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.71.Final/258d8d0ae50a6dc86cb7e8bf4a0599a19c4d81a6/netty-buffer-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.71.Final/f775ec95e32721f5f4bda7b03db22d54af31466e/netty-buffer-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.71.Final/f9c240be81796f4161f842ed5a50159635e4b621/netty-codec-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.71.Final/24fdc51ce7511ad0a350214a1f9bfe046603db17/netty-codec-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.71.Final/c83d4c0249a46a72c7517a1fed73034b01909a8c/netty-codec-dns-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.71.Final/364b0da4f0f8765524a45ad73b3fd0c967c6e5e8/netty-codec-dns-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-haproxy/4.1.71.Final/c87961e46e4954a1bd08ab56a75dc4e0896cba2c/netty-codec-haproxy-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-haproxy/4.1.71.Final/be254ca4ce9fd07006e507b1c8cb89ee1fbb02dc/netty-codec-haproxy-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.71.Final/8348b70fc1122d53c9d610b1142d427e0498c254/netty-codec-http-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.71.Final/7b2203aab395340cc542157eb2dc7a16cffe9a1a/netty-codec-http-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.71.Final/4183c2585fbb39e8b1fa0be4df555b547f388224/netty-codec-http2-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.71.Final/bd69f23e68ede30bfd2232e2473129555ea592e6/netty-codec-http2-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-memcache/4.1.71.Final/11cd190efc6630276c9ef7987818908b4969f76/netty-codec-memcache-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-memcache/4.1.71.Final/202faae4ada353f287ccfcd4d2f05948c44bb796/netty-codec-memcache-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-mqtt/4.1.71.Final/7f1ece34442b861146a2df537c75d6c7fb3429/netty-codec-mqtt-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-mqtt/4.1.71.Final/fb892a7a47ca26d085816f27340cfa8e946b80e8/netty-codec-mqtt-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-redis/4.1.71.Final/2957506e7956762c385368f2eebdc15995dc3723/netty-codec-redis-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-redis/4.1.71.Final/37997fcea14d0ae2edfb5ab4bd2dd00d94ac5e0a/netty-codec-redis-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-smtp/4.1.71.Final/6410470024a9a3d0180ffffb425369758f85270a/netty-codec-smtp-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-smtp/4.1.71.Final/ab4cb436094b86530cb7b02b9621f173e911d506/netty-codec-smtp-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.71.Final/5805dc9258561523d86194c71f27f073d8290def/netty-codec-socks-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.71.Final/d6e6689cf2210621e74fd0faed1ff0eb12e8f254/netty-codec-socks-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-stomp/4.1.71.Final/32ecfa00d63f13ebf6abc49d90db8c93acb6a61e/netty-codec-stomp-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-stomp/4.1.71.Final/c54fefd79298e7f0876f6628a18125ab45686e23/netty-codec-stomp-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-xml/4.1.71.Final/14582c55d9c2cf2152a9e75f64e4ddf947011d69/netty-codec-xml-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-xml/4.1.71.Final/18b3334ad3caefd6afde9ae0ad660d7ec0267bf5/netty-codec-xml-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.71.Final/34ef24f5297849007b8e9ae1884606fd95ef35d4/netty-common-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.71.Final/510df79ad2c1d24fa7a5bf88fddd8cc5bb194ef2/netty-common-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.71.Final/fff73340583f277dd74c7145aa748c72f75a71d/netty-handler-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.71.Final/584e59d7caab701ae81ce86ee27a85a37f7c5a97/netty-handler-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-tcnative-classes/2.0.46.Final/9937a832d9c19861822d345b48ced388b645aa5f/netty-tcnative-classes-2.0.46.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-tcnative-classes/2.0.46.Final/fd5373f574ac969627d50ee013c81384ec99ec5e/netty-tcnative-classes-2.0.46.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.71.Final/67eb43b6306d1dbc5ccc3ba03a38d9209b6ecc42/netty-handler-proxy-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.71.Final/fe3c2143f79a3670a8892645f9e3f747f3dc39cf/netty-handler-proxy-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.71.Final/d4b5377a9bf78015125d5da070b48a4d8d85433e/netty-resolver-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.71.Final/2050e67851609465c34122c5854fd0a7668b5aa/netty-resolver-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.71.Final/89e15f63a6d93ff392cf61704fc6a8361252ae6b/netty-resolver-dns-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.71.Final/94e51791bdb84b5d8ddea02306abc7fa56f51e9e/netty-resolver-dns-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.71.Final/c7d0d89b26b844fabfba68093945fb2703d89761/netty-transport-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.71.Final/3e12d0992805c120c63de2e74c42087e7d2ab0fe/netty-transport-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-rxtx/4.1.71.Final/c4016b4611a62babfb5da937fbcdc1e046bc27df/netty-transport-rxtx-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-rxtx/4.1.71.Final/281abf4f5803e89270a360651eb4a31ef4d83d0/netty-transport-rxtx-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-sctp/4.1.71.Final/473decc08e25a56ad4b4ae7b67efaac7f320738f/netty-transport-sctp-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-sctp/4.1.71.Final/4fda9884d382a7618e21e633323a651dc9f34a1a/netty-transport-sctp-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-udt/4.1.71.Final/33cd943c7ad645e6c3f1f6ac3a3bd24a0e967a37/netty-transport-udt-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-udt/4.1.71.Final/8d1508b1d67f1948fab938ad4cb473ecf19ce461/netty-transport-udt-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.71.Final/c82ed86945dc91fcbacdad7660746657429a67ad/netty-transport-classes-epoll-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.71.Final/877a00bc0a03fb1829ddeadb7e88a31ee8a498dc/netty-transport-classes-epoll-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.71.Final/d7adb0d02ce6bb20a792600b52e353aec65dae86/netty-transport-native-unix-common-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.71.Final/1fcf098126587648d5b8925715d47dd2d0e641cb/netty-transport-native-unix-common-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.71.Final/198df3a7c43c8e7b2c41c8ae9cf3175c30945e0b/netty-transport-classes-kqueue-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.71.Final/ed18a73c341e1d15f9c1bf87a6a7500fc355b004/netty-transport-classes-kqueue-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.71.Final/e4ff273f3bbe438f39b0ba14ee1d981a86b17845/netty-resolver-dns-classes-macos-4.1.71.Final.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.71.Final/35d17a0161ebdc9b20d94e6f2790f5de1740ca28/netty-resolver-dns-classes-macos-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.28.0-GA/9a958811a88381bb159cc2f5ed79c34a45c4af7a/javassist-3.28.0-GA.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.28.0-GA/4e112a71eca1bebcadd85c5a07a9a26265eb12f4/javassist-3.28.0-GA-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.github.spotbugs/spotbugs-annotations/3.1.9/2ef5127efcc1a899aab8c66d449a631c9a99c469/spotbugs-annotations-3.1.9.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.github.spotbugs/spotbugs-annotations/3.1.9/6f3558917456604d0d70f416c4a834db8e579843/spotbugs-annotations-3.1.9-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/b19b5927c2c25b6c70f093767041e641ae0b1b35/jsr305-3.0.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-legacy/4.2.2/26df40b6d9e8ccb823c29dcf23ba284e41c9bf16/mongodb-driver-legacy-4.2.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-legacy/4.2.2/e06ebf99b2c3ac1209a60be17522897f45a53c12/mongodb-driver-legacy-4.2.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-sync/4.2.2/701dc782a41912284c21a76b35e96473550fa5cf/mongodb-driver-sync-4.2.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-sync/4.2.2/6be22c59bec7d4de0fbf55909cb6678bbe7d7ad9/mongodb-driver-sync-4.2.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.github.classgraph/classgraph/4.8.78/87ced5cc1843e8c1736d9abc4a481e75558edddb/classgraph-4.8.78.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.github.classgraph/classgraph/4.8.78/6f434d8d9a59dd004099ee2daa6d374834361e9f/classgraph-4.8.78-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.11.3/c2a818c3b71127167edff8b9dd1caa946f1c8c49/byte-buddy-1.11.3.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.11.3/fedff48c9c601470f4d31cbc96e16450334b056c/byte-buddy-1.11.3-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/5991ca87ef1fb5544943d9abc5a9a37583fabe03/annotations-13.0-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/c3p0/0.9.5.4/a21a1d37ae0b59efce99671544f51c34ed1e8def/c3p0-0.9.5.4.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/c3p0/0.9.5.4/c3bebeb3a803901e8bb0707cb85abe22232fee9a/c3p0-0.9.5.4-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/mchange-commons-java/0.2.15/6ef5abe5f1b94ac45b7b5bad42d871da4fda6bbc/mchange-commons-java-0.2.15.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/mchange-commons-java/0.2.15/dbcfc59448950a4899d9ad88f0a916d5f1e420a0/mchange-commons-java-0.2.15-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-core/4.2.2/1b67153f73a3bcaa94b204cd40543e377ccd7c02/mongodb-driver-core-4.2.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-core/4.2.2/bb57fdf7ab9287c19da557cbfe798b8157e4287f/mongodb-driver-core-4.2.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/bson/4.2.2/56413d45800ef7391bb038095996f1a659cd8a80/bson-4.2.2.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/bson/4.2.2/d6cbe13a51e8871a51e3fe9d74526ac3c8fa6584/bson-4.2.2-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://$MODULE_DIR$/lib/fastutil-mini-8.5.6.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library> + <CLASSES> + <root url="jar://$MODULE_DIR$/lib/kcp-netty.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/2241265a918eddaace47d238422950cc17370571/netty-transport-native-epoll-4.1.71.Final-linux-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/39cbedb9312eae8d9e4622c313f22dfef4106149/netty-transport-native-epoll-4.1.71.Final-linux-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/a7e9bbf9c30db70d5b22ed4c8208b156fcf9cc89/netty-transport-native-kqueue-4.1.71.Final-osx-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/70e613f1023e6118097a6431b5c4608605453679/netty-transport-native-kqueue-4.1.71.Final-osx-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/5f43a8f5d1af42d98d1ed69d8c30f77b4a602747/netty-resolver-dns-native-macos-4.1.71.Final-osx-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/63c9f402684ed981ff254d9ba16fe2433ba38b7e/netty-resolver-dns-native-macos-4.1.71.Final-osx-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/60458f877d055d0c9114d9e1a2efb737b4bc282c/guava-31.1-jre.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/c388a68bc2b17a314dfa7c769d858ada0fc32dcf/guava-31.1-jre-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1d064e61aad6c51cc77f9b59dc2cccc78e792f5a/failureaccess-1.0.1-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5/checker-qual-3.12.0.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/10dacb8b36398debceca36cd0db5f3316967f80e/checker-qual-3.12.0-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/c5a0ace696d3f8b1c1d8cc036d8c03cc0cbe6b69/error_prone_annotations-2.11.0.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/d060e42b6aa896a3abe2ec612e1cf8cc307f8a49/error_prone_annotations-2.11.0-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="RUNTIME"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/d26c56180205cbb50447c3eca98ecb617cf9f58b/j2objc-annotations-1.3-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/2241265a918eddaace47d238422950cc17370571/netty-transport-native-epoll-4.1.71.Final-linux-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/39cbedb9312eae8d9e4622c313f22dfef4106149/netty-transport-native-epoll-4.1.71.Final-linux-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/a7e9bbf9c30db70d5b22ed4c8208b156fcf9cc89/netty-transport-native-kqueue-4.1.71.Final-osx-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/70e613f1023e6118097a6431b5c4608605453679/netty-transport-native-kqueue-4.1.71.Final-osx-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/5f43a8f5d1af42d98d1ed69d8c30f77b4a602747/netty-resolver-dns-native-macos-4.1.71.Final-osx-x86_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/63c9f402684ed981ff254d9ba16fe2433ba38b7e/netty-resolver-dns-native-macos-4.1.71.Final-osx-aarch_64.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/60458f877d055d0c9114d9e1a2efb737b4bc282c/guava-31.1-jre.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/c388a68bc2b17a314dfa7c769d858ada0fc32dcf/guava-31.1-jre-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1d064e61aad6c51cc77f9b59dc2cccc78e792f5a/failureaccess-1.0.1-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES/> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5/checker-qual-3.12.0.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/10dacb8b36398debceca36cd0db5f3316967f80e/checker-qual-3.12.0-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/c5a0ace696d3f8b1c1d8cc036d8c03cc0cbe6b69/error_prone_annotations-2.11.0.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/d060e42b6aa896a3abe2ec612e1cf8cc307f8a49/error_prone_annotations-2.11.0-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library> + <CLASSES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar!/"/> + </CLASSES> + <JAVADOC/> + <SOURCES> + <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/d26c56180205cbb50447c3eca98ecb617cf9f58b/j2objc-annotations-1.3-sources.jar!/"/> + </SOURCES> + </library> + </orderEntry> + </component> + <component name="ModuleRootManager"/> +</module> diff --git a/Grasscutter.ipr b/Grasscutter.ipr new file mode 100644 index 000000000..3759484a6 --- /dev/null +++ b/Grasscutter.ipr @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <option name="DEFAULT_COMPILER" value="Javac"/> + <resourceExtensions> + <entry name=".+\.(properties|xml|html|dtd|tld)"/> + <entry name=".+\.(gif|png|jpeg|jpg)"/> + </resourceExtensions> + <wildcardResourcePatterns> + <entry name="!?*.class"/> + <entry name="!?*.scala"/> + <entry name="!?*.groovy"/> + <entry name="!?*.java"/> + </wildcardResourcePatterns> + <annotationProcessing enabled="false" useClasspath="true"/> + <bytecodeTargetLevel target="17"/> + </component> + <component name="CopyrightManager" default=""> + <module2copyright/> + </component> + <component name="DependencyValidationManager"> + <option name="SKIP_IMPORT_STATEMENTS" value="false"/> + </component> + <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"/> + <component name="GradleUISettings"> + <setting name="root"/> + </component> + <component name="GradleUISettings2"> + <setting name="root"/> + </component> + <component name="IdProvider" IDEtalkID="11DA1DB66DD62DDA1ED602B7079FE97C"/> + <component name="JavadocGenerationManager"> + <option name="OUTPUT_DIRECTORY"/> + <option name="OPTION_SCOPE" value="protected"/> + <option name="OPTION_HIERARCHY" value="true"/> + <option name="OPTION_NAVIGATOR" value="true"/> + <option name="OPTION_INDEX" value="true"/> + <option name="OPTION_SEPARATE_INDEX" value="true"/> + <option name="OPTION_DOCUMENT_TAG_USE" value="false"/> + <option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false"/> + <option name="OPTION_DOCUMENT_TAG_VERSION" value="false"/> + <option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true"/> + <option name="OPTION_DEPRECATED_LIST" value="true"/> + <option name="OTHER_OPTIONS" value=""/> + <option name="HEAP_SIZE"/> + <option name="LOCALE"/> + <option name="OPEN_IN_BROWSER" value="true"/> + </component> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/Grasscutter.iml" filepath="$PROJECT_DIR$/Grasscutter.iml"/> + </modules> + </component> + <component name="ProjectRootManager" version="2" languageLevel="JDK_17" assert-keyword="true" jdk-15="true" project-jdk-type="JavaSDK" assert-jdk-15="true" project-jdk-name="17"> + <output url="file://$PROJECT_DIR$/out"/> + </component> + <component name="SvnBranchConfigurationManager"> + <option name="mySupportsUserInfoFilter" value="true"/> + </component> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs=""/> + </component> + <component name="masterDetails"> + <states> + <state key="ArtifactsStructureConfigurable.UI"> + <UIState> + <splitter-proportions> + <SplitterProportionsDataImpl/> + </splitter-proportions> + <settings/> + </UIState> + </state> + <state key="Copyright.UI"> + <UIState> + <splitter-proportions> + <SplitterProportionsDataImpl/> + </splitter-proportions> + </UIState> + </state> + <state key="ProjectJDKs.UI"> + <UIState> + <splitter-proportions> + <SplitterProportionsDataImpl> + <option name="proportions"> + <list> + <option value="0.2"/> + </list> + </option> + </SplitterProportionsDataImpl> + </splitter-proportions> + <last-edited>1.6</last-edited> + </UIState> + </state> + <state key="ScopeChooserConfigurable.UI"> + <UIState> + <splitter-proportions> + <SplitterProportionsDataImpl/> + </splitter-proportions> + <settings/> + </UIState> + </state> + </states> + </component> +</project> diff --git a/Grasscutter.iws b/Grasscutter.iws new file mode 100644 index 000000000..d5bc7591f --- /dev/null +++ b/Grasscutter.iws @@ -0,0 +1,207 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ChangeListManager"> + <option name="TRACKING_ENABLED" value="true"/> + <option name="SHOW_DIALOG" value="false"/> + <option name="HIGHLIGHT_CONFLICTS" value="true"/> + <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false"/> + <option name="LAST_RESOLUTION" value="IGNORE"/> + </component> + <component name="ChangesViewManager" flattened_view="true" show_ignored="false"/> + <component name="CreatePatchCommitExecutor"> + <option name="PATCH_PATH" value=""/> + <option name="REVERSE_PATCH" value="false"/> + </component> + <component name="DaemonCodeAnalyzer"> + <disable_hints/> + </component> + <component name="DebuggerManager"> + <breakpoint_any> + <breakpoint> + <option name="NOTIFY_CAUGHT" value="true"/> + <option name="NOTIFY_UNCAUGHT" value="true"/> + <option name="ENABLED" value="false"/> + <option name="LOG_ENABLED" value="false"/> + <option name="LOG_EXPRESSION_ENABLED" value="false"/> + <option name="SUSPEND_POLICY" value="SuspendAll"/> + <option name="COUNT_FILTER_ENABLED" value="false"/> + <option name="COUNT_FILTER" value="0"/> + <option name="CONDITION_ENABLED" value="false"/> + <option name="CLASS_FILTERS_ENABLED" value="false"/> + <option name="INSTANCE_FILTERS_ENABLED" value="false"/> + <option name="CONDITION" value=""/> + <option name="LOG_MESSAGE" value=""/> + </breakpoint> + <breakpoint> + <option name="NOTIFY_CAUGHT" value="true"/> + <option name="NOTIFY_UNCAUGHT" value="true"/> + <option name="ENABLED" value="false"/> + <option name="LOG_ENABLED" value="false"/> + <option name="LOG_EXPRESSION_ENABLED" value="false"/> + <option name="SUSPEND_POLICY" value="SuspendAll"/> + <option name="COUNT_FILTER_ENABLED" value="false"/> + <option name="COUNT_FILTER" value="0"/> + <option name="CONDITION_ENABLED" value="false"/> + <option name="CLASS_FILTERS_ENABLED" value="false"/> + <option name="INSTANCE_FILTERS_ENABLED" value="false"/> + <option name="CONDITION" value=""/> + <option name="LOG_MESSAGE" value=""/> + </breakpoint> + </breakpoint_any> + <breakpoint_rules/> + <ui_properties/> + </component> + <component name="ModuleEditorState"> + <option name="LAST_EDITED_MODULE_NAME"/> + <option name="LAST_EDITED_TAB_NAME"/> + </component> + <component name="ProjectInspectionProfilesVisibleTreeState"> + <entry key="Project Default"> + <profile-state/> + </entry> + </component> + <component name="ProjectLevelVcsManager"> + <OptionsSetting value="true" id="Add"/> + <OptionsSetting value="true" id="Remove"/> + <OptionsSetting value="true" id="Checkout"/> + <OptionsSetting value="true" id="Update"/> + <OptionsSetting value="true" id="Status"/> + <OptionsSetting value="true" id="Edit"/> + <ConfirmationsSetting value="0" id="Add"/> + <ConfirmationsSetting value="0" id="Remove"/> + </component> + <component name="ProjectReloadState"> + <option name="STATE" value="0"/> + </component> + <component name="PropertiesComponent"> + <property name="GoToFile.includeJavaFiles" value="false"/> + <property name="GoToClass.toSaveIncludeLibraries" value="false"/> + <property name="MemberChooser.sorted" value="false"/> + <property name="MemberChooser.showClasses" value="true"/> + <property name="GoToClass.includeLibraries" value="false"/> + <property name="MemberChooser.copyJavadoc" value="false"/> + </component> + <component name="RunManager"> + <configuration default="true" type="Remote" factoryName="Remote"> + <option name="USE_SOCKET_TRANSPORT" value="true"/> + <option name="SERVER_MODE" value="false"/> + <option name="SHMEM_ADDRESS" value="javadebug"/> + <option name="HOST" value="localhost"/> + <option name="PORT" value="5005"/> + <method> + <option name="BuildArtifacts" enabled="false"/> + </method> + </configuration> + <configuration default="true" type="Applet" factoryName="Applet"> + <module name=""/> + <option name="MAIN_CLASS_NAME"/> + <option name="HTML_FILE_NAME"/> + <option name="HTML_USED" value="false"/> + <option name="WIDTH" value="400"/> + <option name="HEIGHT" value="300"/> + <option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy"/> + <option name="VM_PARAMETERS"/> + <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> + <option name="ALTERNATIVE_JRE_PATH"/> + <method> + <option name="BuildArtifacts" enabled="false"/> + <option name="Make" enabled="true"/> + </method> + </configuration> + <configuration default="true" type="Application" factoryName="Application"> + <extension name="coverage" enabled="false" merge="false"/> + <option name="MAIN_CLASS_NAME"/> + <option name="VM_PARAMETERS"/> + <option name="PROGRAM_PARAMETERS"/> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/> + <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> + <option name="ALTERNATIVE_JRE_PATH"/> + <option name="ENABLE_SWING_INSPECTOR" value="false"/> + <option name="ENV_VARIABLES"/> + <option name="PASS_PARENT_ENVS" value="true"/> + <module name=""/> + <envs/> + <method> + <option name="BuildArtifacts" enabled="false"/> + <option name="Make" enabled="true"/> + </method> + </configuration> + <configuration default="true" type="JUnit" factoryName="JUnit"> + <extension name="coverage" enabled="false" merge="false"/> + <module name=""/> + <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> + <option name="ALTERNATIVE_JRE_PATH"/> + <option name="PACKAGE_NAME"/> + <option name="MAIN_CLASS_NAME"/> + <option name="METHOD_NAME"/> + <option name="TEST_OBJECT" value="class"/> + <option name="VM_PARAMETERS"/> + <option name="PARAMETERS"/> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/> + <option name="ENV_VARIABLES"/> + <option name="PASS_PARENT_ENVS" value="true"/> + <option name="TEST_SEARCH_SCOPE"> + <value defaultName="moduleWithDependencies"/> + </option> + <envs/> + <method> + <option name="BuildArtifacts" enabled="false"/> + <option name="Make" enabled="true"/> + </method> + </configuration> + <list size="0"/> + <configuration name="<template>" type="WebApp" default="true" selected="false"> + <Host>localhost</Host> + <Port>5050</Port> + </configuration> + </component> + <component name="ShelveChangesManager" show_recycled="false"/> + <component name="SvnConfiguration" maxAnnotateRevisions="500"> + <option name="USER" value=""/> + <option name="PASSWORD" value=""/> + <option name="LAST_MERGED_REVISION"/> + <option name="UPDATE_RUN_STATUS" value="false"/> + <option name="MERGE_DRY_RUN" value="false"/> + <option name="MERGE_DIFF_USE_ANCESTRY" value="true"/> + <option name="UPDATE_LOCK_ON_DEMAND" value="false"/> + <option name="IGNORE_SPACES_IN_MERGE" value="false"/> + <option name="DETECT_NESTED_COPIES" value="true"/> + <option name="IGNORE_SPACES_IN_ANNOTATE" value="true"/> + <option name="SHOW_MERGE_SOURCES_IN_ANNOTATE" value="true"/> + <myIsUseDefaultProxy>false</myIsUseDefaultProxy> + </component> + <component name="TaskManager"> + <task active="true" id="Default" summary="Default task"/> + <servers/> + </component> + <component name="VcsManagerConfiguration"> + <option name="OFFER_MOVE_TO_ANOTHER_CHANGELIST_ON_PARTIAL_COMMIT" value="true"/> + <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="true"/> + <option name="PERFORM_UPDATE_IN_BACKGROUND" value="true"/> + <option name="PERFORM_COMMIT_IN_BACKGROUND" value="true"/> + <option name="PERFORM_EDIT_IN_BACKGROUND" value="true"/> + <option name="PERFORM_CHECKOUT_IN_BACKGROUND" value="true"/> + <option name="PERFORM_ADD_REMOVE_IN_BACKGROUND" value="true"/> + <option name="PERFORM_ROLLBACK_IN_BACKGROUND" value="false"/> + <option name="CHECK_LOCALLY_CHANGED_CONFLICTS_IN_BACKGROUND" value="false"/> + <option name="ENABLE_BACKGROUND_PROCESSES" value="false"/> + <option name="CHANGED_ON_SERVER_INTERVAL" value="60"/> + <option name="FORCE_NON_EMPTY_COMMENT" value="false"/> + <option name="LAST_COMMIT_MESSAGE"/> + <option name="MAKE_NEW_CHANGELIST_ACTIVE" value="true"/> + <option name="OPTIMIZE_IMPORTS_BEFORE_PROJECT_COMMIT" value="false"/> + <option name="CHECK_FILES_UP_TO_DATE_BEFORE_COMMIT" value="false"/> + <option name="REFORMAT_BEFORE_PROJECT_COMMIT" value="false"/> + <option name="REFORMAT_BEFORE_FILE_COMMIT" value="false"/> + <option name="FILE_HISTORY_DIALOG_COMMENTS_SPLITTER_PROPORTION" value="0.8"/> + <option name="FILE_HISTORY_DIALOG_SPLITTER_PROPORTION" value="0.5"/> + <option name="ACTIVE_VCS_NAME"/> + <option name="UPDATE_GROUP_BY_PACKAGES" value="false"/> + <option name="UPDATE_GROUP_BY_CHANGELIST" value="false"/> + <option name="SHOW_FILE_HISTORY_AS_TREE" value="false"/> + <option name="FILE_HISTORY_SPLITTER_PROPORTION" value="0.6"/> + </component> + <component name="XDebuggerManager"> + <breakpoint-manager/> + </component> +</project> diff --git a/grasscutter-1.0.3-dev.jar.asc b/grasscutter-1.0.3-dev.jar.asc new file mode 100644 index 000000000..0249d0297 --- /dev/null +++ b/grasscutter-1.0.3-dev.jar.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP SIGNATURE----- +Version: BCPG v1.68 + +iQGcBAABCgAGBQJicbItAAoJEK1DoRSUkxpGzZgMAJaxuuXmG3V1gFdJLXoKR+6s +moOjwyD8UDfFModX92Thccgox9/j72A1x8sKQfJNDzI5wx51iR2rXw52KE6GhdVI +vSyhnGv6LGOtGtA59i8wnXcEKBD33Qm6B2KFD4ox4JEheMb/wWK3zF09aknLkUVX +43L48E4dF0lAzJ7QWZTTNKCK156Bwa3F8NhVLGGD6tpGahkS8J73Ax6C8uu6zVKf +8dftBpI+0YxPJkbxoPahVZVmFawUjcjPDcRwG5LTO6McVUI9YzczSHdk0FG39ENo +HXvwsK/xnN2Xy8ta+ylu9Eta0zx9mLTZjEjUQ3B8XjTDDVcz11DlvE5L1jJ8Gov+ +XbCM0m+od0hziCwuYg2BOsi13C9vejA5BoCHeejNTy+QiGGhK4QdyxdufxQD1Bo4 +uF8ZmmeC1AMA7m1y4tqIqwA5iJQx4KaB3aKw8np0bYuVVNnw75wpf3NBcQKW/Jf7 +diKjcimqhSkPpJ/ok0ZqITpMTaYhZoXnyUFsm0DIHA== +=5WgW +-----END PGP SIGNATURE----- diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json new file mode 100644 index 000000000..22d1339c8 --- /dev/null +++ b/src/main/resources/languages/zh-TW.json @@ -0,0 +1,298 @@ +{ + "messages": { + "game": { + "port_bind": "遊戲伺服器已成功啟動。端口號:%s", + "connect": "客戶端已連接至 %s", + "disconnect": "客戶端 %s 已斷開連接。", + "game_update_error": "遊戲更新時發生了錯誤。", + "command_error": "指令發生錯誤:" + }, + "dispatch": { + "port_bind": "[Dispatch] 伺服器已在端口 %s 上開啟。", + "request": "[Dispatch] 客戶端 %s 請求: %s %s", + "keystore": { + "general_error": "[Dispatch] 加載keystore文件時發生錯誤!", + "password_error": "[Dispatch] 加載 keystore 失敗。正在嘗試使用預設 keystore 密碼...", + "no_keystore_error": "[Dispatch] 未找到 SSL 憑證!已後降到 HTTP 伺服器。", + "default_password": "[Dispatch] 默認的 keystore 密碼加載成功。請考慮將 config.json 的憑證密碼設定成 123456。" + }, + "no_commands_error": "此指令不適用於Dispatch-only模式。", + "unhandled_request_error": "[Dispatch] 潛在的未處理請求 %s 請求:%s", + "account": { + "login_attempt": "[Dispatch] 客戶端 %s 正在嘗試登入", + "login_success": "[Dispatch] 客戶端 %s 已登入,UID為 %s", + "login_token_attempt": "[Dispatch] 客戶端 %s 正在嘗試用憑證登入", + "login_token_error": "[Dispatch] 客戶端 %s 使用憑證登入失敗", + "login_token_success": "[Dispatch] 客戶端 %s 已透過憑證登入,UID為 %s", + "combo_token_success": "[Dispatch] 客戶端 %s 交換憑證成功", + "combo_token_error": "[Dispatch] 客戶端 %s 交換憑證失敗", + "account_login_create_success": "[Dispatch] 客戶端 %s 登入失敗: 已註冊UID為 %s 的帳號", + "account_login_create_error": "[Dispatch] 客戶端 %s 登入失敗:帳號建立失敗。", + "account_login_exist_error": "[Dispatch] 客戶端 %s 登入失敗: 帳號不存在", + "account_cache_error": "遊戲帳號緩存資訊錯誤", + "session_key_error": "對話密鑰不符。", + "username_error": "未找到此用戶名。", + "username_create_error": "未找到用戶名,建立失敗。" + } + }, + "status": { + "free_software": "Grasscutter 是免費開源軟體。如果你已經付錢了,那你可能被騙了。主頁:https://github.com/Grasscutters/Grasscutter", + "starting": "正在啟動 Grasscutter...", + "shutdown": "正在關閉...", + "done": "加載完成!需要指令幫助請輸入 \"help\"", + "error": "發生了一個錯誤。", + "welcome": "歡迎使用 Grasscutter", + "run_mode_error": "無效的伺服器運行模式: %s。", + "run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...", + "create_resources": "正在建立 resources 資料夾...", + "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。" + } + }, + "commands": { + "generic": { + "not_specified": "沒有指定指令。", + "unknown_command": "未知的指令:%s", + "permission_error": "您沒有執行此指令的權限。", + "console_execute_error": "此指令只能在伺服器的命令提示字元執行。", + "player_execute_error": "請在遊戲裡使用這條指令。", + "command_exist_error": "找不到指令。", + "invalid": { + "amount": "無效的 數量.", + "artifactId": "無效的聖遺物ID。", + "avatarId": "無效的角色ID。", + "avatarLevel": "無效的角色等級。", + "entityId": "無效的實體ID。", + "itemId": "無效的物品ID。", + "itemLevel": "無效的物品等級。", + "itemRefinement": "無效的物品精煉度。", + "playerId": "無效的玩家ID。", + "uid": "無效的UID。" + } + }, + "execution": { + "uid_error": "無效的UID。", + "player_exist_error": "用戶不存在。", + "player_offline_error": "玩家已離線。", + "item_id_error": "無效的物品ID。.", + "item_player_exist_error": "無效的物品/玩家UID。", + "entity_id_error": "無效的實體ID。", + "player_exist_offline_error": "玩家不存在或已離線。", + "argument_error": "無效的參數。", + "clear_target": "目標已清除.", + "set_target": "隨後的指令都會以@%s為預設。", + "need_target": "此指令需要一個目標 UID。添加 <@UID> 引數或者使用 /target @UID 來設定持久目標。" + }, + "status": { + "enabled": "已啟用", + "disabled": "未啟用", + "help": "幫助", + "success": "成功" + }, + "account": { + "modify": "修改使用者帳號", + "invalid": "無效的UID。", + "exists": "帳號已存在。", + "create": "已建立帳號,UID 為 %s 。", + "delete": "帳號已刪除。", + "no_account": "帳號不存在。", + "command_usage": "用法:account <create|delete> <username> [uid]" + }, + "broadcast": { + "command_usage": "用法:broadcast <message>", + "message_sent": "公告已發送。" + }, + "changescene": { + "usage": "用法:changescene <scene id>", + "already_in_scene": "你已經在這個場景中了。", + "success": "已切換至場景 %s.", + "exists_error": "此場景不存在。" + }, + "clear": { + "command_usage": "用法: clear <all|wp|art|mat>", + "weapons": "已將 %s 的武器清空。", + "artifacts": "已將 %s 的聖遺物清空。", + "materials": "已將 %s 的材料清空。", + "furniture": "已將 %s 的塵歌壺家具清空。", + "displays": "已清除 %s 的顯示。", + "virtuals": "已將 %s 的所有貨幣和經驗值清空。", + "everything": "已將 %s 的所有物品清空。" + }, + "coop": { + "usage": "用法:coop <playerId> <target playerId>", + "success": "Summoned %s to %s's world." + }, + "enter_dungeon": { + "usage": "用法:enterdungeon <dungeon id>", + "changed": "已進入副本 %s", + "not_found_error": "此副本不存在。", + "in_dungeon_error": "你已經在祕境中了。" + }, + "giveAll": { + "usage": "用法:giveall [player] [amount]", + "started": "正在賦予全部物品...", + "success": "已賦予全部物品。", + "invalid_amount_or_playerId": "無效的數量/玩家ID。" + }, + "giveArtifact": { + "usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "id_error": "無效的聖遺物ID。", + "success": "已把 %s 給予 %s。" + }, + "giveChar": { + "usage": "用法:givechar <player> <itemId|itemName> [amount]", + "given": "Given %s with level %s to %s.", + "invalid_avatar_id": "無效的角色ID。", + "invalid_avatar_level": "無效的角色等級。.", + "invalid_avatar_or_player_id": "無效的角色ID/玩家ID。" + }, + "give": { + "usage": "用法:give <player> <itemId|itemName> [amount] [level]", + "refinement_only_applicable_weapons": "精煉度只能施加在武器上面。", + "refinement_must_between_1_and_5": "精煉度必需在 1 到 5 之間。", + "given": "已經將 %s 個 %s 給予 %s。", + "given_with_level_and_refinement": "已將 %s [等級%s, 精煉%s] %s個給予 %s", + "given_level": "已將 %s 等級 %s %s 個給予 %s" + }, + "godmode": { + "success": "上帝模式設定為 %s 。 [用戶:%s]" + }, + "heal": { + "success": "所有角色已被治療。" + }, + "kick": { + "player_kick_player": "玩家 [%s:%s] 已把 [%s:%s] 踢出", + "server_kick_player": "正在踢出玩家 [%s:%s]" + }, + "kill": { + "usage": "用法:killall [playerUid] [sceneId]", + "scene_not_found_in_player_world": "未在玩家世界中找到此場景", + "kill_monsters_in_scene": "已殺死 %s 個怪物。 [場景ID: %s]" + }, + "killCharacter": { + "usage": "用法:/killcharacter [playerId]", + "success": "已殺死 %s 目前的場上角色。" + }, + "list": { + "message": "目前總線上人數:%s" + }, + "permission": { + "usage": "用法:permission <add|remove> <username> <permission>", + "add": "已指派權限。", + "has_error": "此玩家已擁有權限!", + "remove": "權限已移除。", + "not_have_error": "此玩家未擁有權限!", + "account_error": "The account cannot be found." + }, + "position": { + "success": "坐標:%.3f, %.3f, %.3f\n場景ID:%d" + }, + "reload": { + "reload_start": "正在重新加載設定檔。", + "reload_done": "重新加載已完成。" + }, + "resetConst": { + "reset_all": "重設所有角色的命座。", + "success": "已重設 %s 的命座,重新登入後將會生效。" + }, + "resetShopLimit": { + "usage": "用法:/resetshop <player id>" + }, + "sendMail": { + "usage": "用法:give [player] <itemId|itemName> [amount]", + "user_not_exist": "ID '%s' 的使用者不存在。", + "start_composition": "發送郵件流程。\n請使用`/send <郵件標題>`來進到下一步。\n你可以在任何時間使用`/sendmail stop`來停止發送。", + "templates": "郵件模板尚未實裝...", + "invalid_arguments": "無效的參數。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`", + "send_cancel": "取消傳送信息", + "send_done": "已將消息發送給 %s!", + "send_all_done": "消息已發送給全體用戶!", + "not_composition_end": "現在郵件發送未到最後階段。\n請使用 `/sendmail %s` 繼續發送郵件,或者 `/sendmail stop` 來停止發送郵件。", + "please_use": "請使用 `/sendmail %s`", + "set_title": "成功將郵件標題設定成 '%s'。\n接下來請繼續使用 '/sendmail <content>' 來設定郵件內容。", + "set_contents": "成功將'%s'為郵件內容。\n接下來請打出 '/sendmail <寄件者名稱>' 來設定郵件寄件者名稱。", + "set_message_sender": "郵件寄件者已設為 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 以繼續操作。", + "send": "已添加 %s 個 %s (等級為 %s) 到郵件附件。\n如果沒有要繼續添加道具請使用 `/sendmail finish` 來完成郵件發送。", + "invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`", + "title": "<標題>", + "message": "<正文>", + "sender": "<寄件者>", + "arguments": "<itemId|itemName|finish> [數量] [等級]", + "error": "錯誤:無效的編寫階段 %s。需要 stacktrace 請查看伺服器命令提示字元。" + }, + "sendMessage": { + "usage": "用法:sendmessage <player> <message>", + "success": "訊息已發送。" + }, + "setFetterLevel": { + "usage": "用法:setfetterlevel <level>", + "range_error": "好感度必須在 0 到 10 之間。", + "fetter_set_level": "好感等級已設定為 %s", + "level_error": "無效的好感度。" + }, + "setStats": { + "usage_console": "用法:setstats|stats @<UID> <stat> <value>", + "usage_ingame": "用法:setstats|stats [@UID] <stat> <value>", + "help_message": "\n\t可使用的數據類型:hp (生命值)| maxhp (最大生命值) | def(防禦力) | atk (攻擊力)| em (元素精通) | er (元素充能效率) | crate(暴擊率) | cdmg (暴擊傷害)| cdr (冷卻縮減) | heal(治療加成)| heali (受治療加成)| shield (護盾強效)| defi (無視防禦)\n\t(cont.) 元素增傷類:epyro (火傷) | ecryo (冰傷) | ehydro (水傷) | egeo (岩傷) | edendro (草傷) | eelectro (雷傷) | ephys (物傷)(cont.) 元素減傷類:respyro (火抗) | rescryo (冰抗) | reshydro (水抗) | resgeo (岩抗) | resdendro (草抗) | reselectro (雷抗) | resphys (物抗)\n", + "value_error": "無效的數據值。", + "uid_error": "無效的UID。", + "player_error": "玩家不存在或已離線。", + "set_self": "%s 已經設為 %s。", + "set_for_uid": "%s 的使用者 %s 更改為 %s。", + "set_max_hp": "最大生命值更改為 %s。" + }, + "setWorldLevel": { + "usage": "用法:setworldlevel <level>", + "value_error": "世界等級必須設定在0-8之間。", + "success": "已將世界等級設為%s。", + "invalid_world_level": "無效的世界等級。" + }, + "spawn": { + "usage": "用法:spawn <entityId> [amount] [level(僅限怪物)]", + "success": "已生成 %s 個 %s。" + }, + "stop": { + "success": "正在關閉伺服器..." + }, + "talent": { + "usage_1": "設定天賦等級:/talent set <talentID> <value>", + "usage_2": "另一種設定天賦等級的指令使用方法:/talent <n or e or q> <value>", + "usage_3": "獲取天賦ID指令用法:/talent getid", + "lower_16": "無效的技能等級,技能等級應低於 16。", + "set_id": "將天賦等級設為%s。", + "set_atk": "將普通攻擊等級設為 %s。", + "set_e": "設定天賦E等級至 %s。", + "set_q": "設定天賦Q等級至 %s。", + "invalid_skill_id": "無效的技能ID。", + "set_this": "將天賦等級設為 %s。", + "invalid_level": "無效的天賦等級。", + "normal_attack_id": "普通攻擊的 ID 為 %s。", + "e_skill_id": "E技能ID %s。", + "q_skill_id": "Q技能ID %s。" + }, + "teleportAll": { + "success": "Summoned all players to your location.", + "error": "此指令僅可在多人遊戲下可用。" + }, + "teleport": { + "usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]", + "usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]", + "specify_player_id": "你必須指定一個玩家ID。", + "invalid_position": "無效的位置。", + "success": "傳送 %s 到坐標 %s,%s,%s ,場景為 %s" + }, + "weather": { + "usage": "用法:weather <weatherId> [climateId]", + "success": "已將當前天氣設定為 %s ,氣候則為 %s 。", + "invalid_id": "無效的ID。" + }, + "drop": { + "command_usage": "用法:drop <itemId|itemName> [amount]", + "success": "已將 %s x %s 丟在附近。" + }, + "help": { + "usage": "用法:", + "aliases": "別名:", + "available_commands": "可用指令:" + } + } +} \ No newline at end of file From 686df18b74bb9cf43f765d1954826df4e01f8750 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 18:57:49 -0400 Subject: [PATCH 142/434] Ensure we have a fallback for localization --- src/main/java/emu/grasscutter/utils/Language.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 702da202a..c4af257e8 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -61,7 +61,7 @@ public final class Language { JsonObject object = this.languageData; int index = 0; - String result = ""; + String result = "This value does not exist. Please report this to the Discord: " + key; while (true) { if(index == keys.length) break; From d8719ecc526ab31666a436470075c3676e408a44 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 18:59:26 -0400 Subject: [PATCH 143/434] Update .gitignore & remove files --- .gitignore | 2 +- Grasscutter.iml | 865 ---------------------------------- Grasscutter.ipr | 104 ---- Grasscutter.iws | 207 -------- grasscutter-1.0.3-dev.jar.asc | 14 - 5 files changed, 1 insertion(+), 1191 deletions(-) delete mode 100644 Grasscutter.iml delete mode 100644 Grasscutter.ipr delete mode 100644 Grasscutter.iws delete mode 100644 grasscutter-1.0.3-dev.jar.asc diff --git a/.gitignore b/.gitignore index 32987345b..8571ccb88 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ tmp/ .vscode # Grasscutter -resources/ +/resources/ logs/ plugins/ data/AbilityEmbryos.json diff --git a/Grasscutter.iml b/Grasscutter.iml deleted file mode 100644 index 0f136c501..000000000 --- a/Grasscutter.iml +++ /dev/null @@ -1,865 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module relativePaths="true" type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" inherit-compiler-output="true"> - <exclude-output/> - <orderEntry type="inheritedJdk"/> - <content url="file://$MODULE_DIR$/"> - <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false"/> - <sourceFolder url="file://$MODULE_DIR$/proto" isTestSource="false"/> - <sourceFolder url="file://$MODULE_DIR$/src/main/proto" isTestSource="false"/> - <sourceFolder url="file://$MODULE_DIR$/src/generated" isTestSource="false"/> - <sourceFolder url="file://$MODULE_DIR$/build/extracted-include-protos/main" isTestSource="false" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/build/extracted-protos/main" isTestSource="false" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/src/generated/main/java" isTestSource="false" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/src/test/proto" isTestSource="true"/> - <sourceFolder url="file://$MODULE_DIR$/build/extracted-include-protos/test" isTestSource="true" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/build/extracted-protos/test" isTestSource="true" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/src/generated/test/java" isTestSource="true" generated="true"/> - <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource"/> - <excludeFolder url="file://$MODULE_DIR$/.gradle"/> - <excludeFolder url="file://$MODULE_DIR$/build"/> - </content> - <orderEntry type="sourceFolder" forTests="false"/> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.9/7d495522b08a9a66084bf417e70eedf95ef706bc/logback-classic-1.2.9.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.9/e62ec78303270aefa40721154dc591b9c86072b8/logback-classic-1.2.9-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.reflections/reflections/0.10.2/b638d7ca0e0fe0146b60a0e7ba232ad852a73b31/reflections-0.10.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.reflections/reflections/0.10.2/68391d7abbca924e397401333c5a9492531812b6/reflections-0.10.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/dev.morphia.morphia/morphia-core/2.2.6/8dd21dd7e49cc78d8e726e236bdcd28510ad776a/morphia-core-2.2.6.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/dev.morphia.morphia/morphia-core/2.2.6/7364d3ec3ff8083a6c8263e26c8459ca9908278f/morphia-core-2.2.6-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz/2.3.2/18a6d6b5a40b77bd060b34cb9f2acadc4bae7c8a/quartz-2.3.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz/2.3.2/c22b720ab9a92367424cda3d628fea758dd7e4cf/quartz-2.3.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP-java7/2.4.13/3e441eddedb374d4de8e3abbb0c90997f51cc97b/HikariCP-java7-2.4.13.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP-java7/2.4.13/fc995c9e7945c5d9efa11fdbb5cbf10606409cd5/HikariCP-java7-2.4.13-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/ae9c1aae0033af915cfa75d850eb9d880f21a701/slf4j-api-1.7.36-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.9/cdaca0cf922c5791a8efa0063ec714ca974affe3/logback-core-1.2.9.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.9/92412f3c48649ab2dbe036dd9a57af0a741cb27e/logback-core-1.2.9-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-all/4.1.71.Final/8c89f16505b815e966bb1d4bf4681bdd3701b8b1/netty-all-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.8/431fc3cbc0ff81abdbfde070062741089c3ba874/gson-2.8.8.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.8/c0f02b42d0474823279fc9606a81338896d59941/gson-2.8.8-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.18.2/17e444501d7ed8dd1f6348f5bc0ad627200defa8/protobuf-java-3.18.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.18.2/2f741dd5fdc324ed91ac2916573a199302397d75/protobuf-java-3.18.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.greenrobot/eventbus-java/3.3.1/74487b0caceca6fcd1aff45d41a9cdc6910d7f5a/eventbus-java-3.3.1.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.greenrobot/eventbus-java/3.3.1/d59e3be28ebbbc065159aabfa9e066edb584baf2/eventbus-java-3.3.1-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.danilopianini/java-quadtree/0.1.9/3eb9cde063327dfefa62e281f7858b44d6d90006/java-quadtree-0.1.9.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.danilopianini/java-quadtree/0.1.9/9a127c827d012257fb39607dc9c56a911e6de482/java-quadtree-0.1.9-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz-jobs/2.3.2/b2223bd809ffc77d41a2739fde85b822e59be2fe/quartz-jobs-2.3.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.quartz-scheduler/quartz-jobs/2.3.2/26358986162924db92d4205754c37d213065f388/quartz-jobs-2.3.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.71.Final/258d8d0ae50a6dc86cb7e8bf4a0599a19c4d81a6/netty-buffer-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.71.Final/f775ec95e32721f5f4bda7b03db22d54af31466e/netty-buffer-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.71.Final/f9c240be81796f4161f842ed5a50159635e4b621/netty-codec-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.71.Final/24fdc51ce7511ad0a350214a1f9bfe046603db17/netty-codec-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.71.Final/c83d4c0249a46a72c7517a1fed73034b01909a8c/netty-codec-dns-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.71.Final/364b0da4f0f8765524a45ad73b3fd0c967c6e5e8/netty-codec-dns-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-haproxy/4.1.71.Final/c87961e46e4954a1bd08ab56a75dc4e0896cba2c/netty-codec-haproxy-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-haproxy/4.1.71.Final/be254ca4ce9fd07006e507b1c8cb89ee1fbb02dc/netty-codec-haproxy-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.71.Final/8348b70fc1122d53c9d610b1142d427e0498c254/netty-codec-http-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.71.Final/7b2203aab395340cc542157eb2dc7a16cffe9a1a/netty-codec-http-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.71.Final/4183c2585fbb39e8b1fa0be4df555b547f388224/netty-codec-http2-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.71.Final/bd69f23e68ede30bfd2232e2473129555ea592e6/netty-codec-http2-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-memcache/4.1.71.Final/11cd190efc6630276c9ef7987818908b4969f76/netty-codec-memcache-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-memcache/4.1.71.Final/202faae4ada353f287ccfcd4d2f05948c44bb796/netty-codec-memcache-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-mqtt/4.1.71.Final/7f1ece34442b861146a2df537c75d6c7fb3429/netty-codec-mqtt-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-mqtt/4.1.71.Final/fb892a7a47ca26d085816f27340cfa8e946b80e8/netty-codec-mqtt-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-redis/4.1.71.Final/2957506e7956762c385368f2eebdc15995dc3723/netty-codec-redis-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-redis/4.1.71.Final/37997fcea14d0ae2edfb5ab4bd2dd00d94ac5e0a/netty-codec-redis-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-smtp/4.1.71.Final/6410470024a9a3d0180ffffb425369758f85270a/netty-codec-smtp-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-smtp/4.1.71.Final/ab4cb436094b86530cb7b02b9621f173e911d506/netty-codec-smtp-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.71.Final/5805dc9258561523d86194c71f27f073d8290def/netty-codec-socks-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.71.Final/d6e6689cf2210621e74fd0faed1ff0eb12e8f254/netty-codec-socks-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-stomp/4.1.71.Final/32ecfa00d63f13ebf6abc49d90db8c93acb6a61e/netty-codec-stomp-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-stomp/4.1.71.Final/c54fefd79298e7f0876f6628a18125ab45686e23/netty-codec-stomp-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-xml/4.1.71.Final/14582c55d9c2cf2152a9e75f64e4ddf947011d69/netty-codec-xml-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-xml/4.1.71.Final/18b3334ad3caefd6afde9ae0ad660d7ec0267bf5/netty-codec-xml-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.71.Final/34ef24f5297849007b8e9ae1884606fd95ef35d4/netty-common-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.71.Final/510df79ad2c1d24fa7a5bf88fddd8cc5bb194ef2/netty-common-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.71.Final/fff73340583f277dd74c7145aa748c72f75a71d/netty-handler-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.71.Final/584e59d7caab701ae81ce86ee27a85a37f7c5a97/netty-handler-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-tcnative-classes/2.0.46.Final/9937a832d9c19861822d345b48ced388b645aa5f/netty-tcnative-classes-2.0.46.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-tcnative-classes/2.0.46.Final/fd5373f574ac969627d50ee013c81384ec99ec5e/netty-tcnative-classes-2.0.46.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.71.Final/67eb43b6306d1dbc5ccc3ba03a38d9209b6ecc42/netty-handler-proxy-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.71.Final/fe3c2143f79a3670a8892645f9e3f747f3dc39cf/netty-handler-proxy-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.71.Final/d4b5377a9bf78015125d5da070b48a4d8d85433e/netty-resolver-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.71.Final/2050e67851609465c34122c5854fd0a7668b5aa/netty-resolver-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.71.Final/89e15f63a6d93ff392cf61704fc6a8361252ae6b/netty-resolver-dns-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.71.Final/94e51791bdb84b5d8ddea02306abc7fa56f51e9e/netty-resolver-dns-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.71.Final/c7d0d89b26b844fabfba68093945fb2703d89761/netty-transport-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.71.Final/3e12d0992805c120c63de2e74c42087e7d2ab0fe/netty-transport-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-rxtx/4.1.71.Final/c4016b4611a62babfb5da937fbcdc1e046bc27df/netty-transport-rxtx-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-rxtx/4.1.71.Final/281abf4f5803e89270a360651eb4a31ef4d83d0/netty-transport-rxtx-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-sctp/4.1.71.Final/473decc08e25a56ad4b4ae7b67efaac7f320738f/netty-transport-sctp-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-sctp/4.1.71.Final/4fda9884d382a7618e21e633323a651dc9f34a1a/netty-transport-sctp-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-udt/4.1.71.Final/33cd943c7ad645e6c3f1f6ac3a3bd24a0e967a37/netty-transport-udt-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-udt/4.1.71.Final/8d1508b1d67f1948fab938ad4cb473ecf19ce461/netty-transport-udt-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.71.Final/c82ed86945dc91fcbacdad7660746657429a67ad/netty-transport-classes-epoll-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.71.Final/877a00bc0a03fb1829ddeadb7e88a31ee8a498dc/netty-transport-classes-epoll-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.71.Final/d7adb0d02ce6bb20a792600b52e353aec65dae86/netty-transport-native-unix-common-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.71.Final/1fcf098126587648d5b8925715d47dd2d0e641cb/netty-transport-native-unix-common-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.71.Final/198df3a7c43c8e7b2c41c8ae9cf3175c30945e0b/netty-transport-classes-kqueue-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.71.Final/ed18a73c341e1d15f9c1bf87a6a7500fc355b004/netty-transport-classes-kqueue-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.71.Final/e4ff273f3bbe438f39b0ba14ee1d981a86b17845/netty-resolver-dns-classes-macos-4.1.71.Final.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.71.Final/35d17a0161ebdc9b20d94e6f2790f5de1740ca28/netty-resolver-dns-classes-macos-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.28.0-GA/9a958811a88381bb159cc2f5ed79c34a45c4af7a/javassist-3.28.0-GA.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.28.0-GA/4e112a71eca1bebcadd85c5a07a9a26265eb12f4/javassist-3.28.0-GA-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.github.spotbugs/spotbugs-annotations/3.1.9/2ef5127efcc1a899aab8c66d449a631c9a99c469/spotbugs-annotations-3.1.9.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.github.spotbugs/spotbugs-annotations/3.1.9/6f3558917456604d0d70f416c4a834db8e579843/spotbugs-annotations-3.1.9-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/b19b5927c2c25b6c70f093767041e641ae0b1b35/jsr305-3.0.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-legacy/4.2.2/26df40b6d9e8ccb823c29dcf23ba284e41c9bf16/mongodb-driver-legacy-4.2.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-legacy/4.2.2/e06ebf99b2c3ac1209a60be17522897f45a53c12/mongodb-driver-legacy-4.2.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-sync/4.2.2/701dc782a41912284c21a76b35e96473550fa5cf/mongodb-driver-sync-4.2.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-sync/4.2.2/6be22c59bec7d4de0fbf55909cb6678bbe7d7ad9/mongodb-driver-sync-4.2.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.github.classgraph/classgraph/4.8.78/87ced5cc1843e8c1736d9abc4a481e75558edddb/classgraph-4.8.78.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.github.classgraph/classgraph/4.8.78/6f434d8d9a59dd004099ee2daa6d374834361e9f/classgraph-4.8.78-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.11.3/c2a818c3b71127167edff8b9dd1caa946f1c8c49/byte-buddy-1.11.3.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.11.3/fedff48c9c601470f4d31cbc96e16450334b056c/byte-buddy-1.11.3-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/5991ca87ef1fb5544943d9abc5a9a37583fabe03/annotations-13.0-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/c3p0/0.9.5.4/a21a1d37ae0b59efce99671544f51c34ed1e8def/c3p0-0.9.5.4.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/c3p0/0.9.5.4/c3bebeb3a803901e8bb0707cb85abe22232fee9a/c3p0-0.9.5.4-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/mchange-commons-java/0.2.15/6ef5abe5f1b94ac45b7b5bad42d871da4fda6bbc/mchange-commons-java-0.2.15.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.mchange/mchange-commons-java/0.2.15/dbcfc59448950a4899d9ad88f0a916d5f1e420a0/mchange-commons-java-0.2.15-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-core/4.2.2/1b67153f73a3bcaa94b204cd40543e377ccd7c02/mongodb-driver-core-4.2.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/mongodb-driver-core/4.2.2/bb57fdf7ab9287c19da557cbfe798b8157e4287f/mongodb-driver-core-4.2.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/bson/4.2.2/56413d45800ef7391bb038095996f1a659cd8a80/bson-4.2.2.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.mongodb/bson/4.2.2/d6cbe13a51e8871a51e3fe9d74526ac3c8fa6584/bson-4.2.2-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://$MODULE_DIR$/lib/fastutil-mini-8.5.6.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://$MODULE_DIR$/lib/kcp-netty.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/2241265a918eddaace47d238422950cc17370571/netty-transport-native-epoll-4.1.71.Final-linux-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/39cbedb9312eae8d9e4622c313f22dfef4106149/netty-transport-native-epoll-4.1.71.Final-linux-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/a7e9bbf9c30db70d5b22ed4c8208b156fcf9cc89/netty-transport-native-kqueue-4.1.71.Final-osx-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/70e613f1023e6118097a6431b5c4608605453679/netty-transport-native-kqueue-4.1.71.Final-osx-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/5f43a8f5d1af42d98d1ed69d8c30f77b4a602747/netty-resolver-dns-native-macos-4.1.71.Final-osx-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/63c9f402684ed981ff254d9ba16fe2433ba38b7e/netty-resolver-dns-native-macos-4.1.71.Final-osx-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/60458f877d055d0c9114d9e1a2efb737b4bc282c/guava-31.1-jre.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/c388a68bc2b17a314dfa7c769d858ada0fc32dcf/guava-31.1-jre-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1d064e61aad6c51cc77f9b59dc2cccc78e792f5a/failureaccess-1.0.1-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5/checker-qual-3.12.0.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/10dacb8b36398debceca36cd0db5f3316967f80e/checker-qual-3.12.0-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/c5a0ace696d3f8b1c1d8cc036d8c03cc0cbe6b69/error_prone_annotations-2.11.0.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/d060e42b6aa896a3abe2ec612e1cf8cc307f8a49/error_prone_annotations-2.11.0-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="RUNTIME"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/d26c56180205cbb50447c3eca98ecb617cf9f58b/j2objc-annotations-1.3-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/2241265a918eddaace47d238422950cc17370571/netty-transport-native-epoll-4.1.71.Final-linux-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/39cbedb9312eae8d9e4622c313f22dfef4106149/netty-transport-native-epoll-4.1.71.Final-linux-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.71.Final/f0ae9b17d5bc17bc6068b966517ae150a66d5a2b/netty-transport-native-epoll-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/a7e9bbf9c30db70d5b22ed4c8208b156fcf9cc89/netty-transport-native-kqueue-4.1.71.Final-osx-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/70e613f1023e6118097a6431b5c4608605453679/netty-transport-native-kqueue-4.1.71.Final-osx-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.71.Final/af5357636d3a98802de915e66be10513cfedfe64/netty-transport-native-kqueue-4.1.71.Final-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/5f43a8f5d1af42d98d1ed69d8c30f77b4a602747/netty-resolver-dns-native-macos-4.1.71.Final-osx-x86_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.71.Final/63c9f402684ed981ff254d9ba16fe2433ba38b7e/netty-resolver-dns-native-macos-4.1.71.Final-osx-aarch_64.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/60458f877d055d0c9114d9e1a2efb737b4bc282c/guava-31.1-jre.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/31.1-jre/c388a68bc2b17a314dfa7c769d858ada0fc32dcf/guava-31.1-jre-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1d064e61aad6c51cc77f9b59dc2cccc78e792f5a/failureaccess-1.0.1-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES/> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/d5692f0526415fcc6de94bb5bfbd3afd9dd3b3e5/checker-qual-3.12.0.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.12.0/10dacb8b36398debceca36cd0db5f3316967f80e/checker-qual-3.12.0-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/c5a0ace696d3f8b1c1d8cc036d8c03cc0cbe6b69/error_prone_annotations-2.11.0.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.11.0/d060e42b6aa896a3abe2ec612e1cf8cc307f8a49/error_prone_annotations-2.11.0-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - <orderEntry type="module-library" scope="TEST"> - <library> - <CLASSES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar!/"/> - </CLASSES> - <JAVADOC/> - <SOURCES> - <root url="jar://C:/Users/kobed/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/d26c56180205cbb50447c3eca98ecb617cf9f58b/j2objc-annotations-1.3-sources.jar!/"/> - </SOURCES> - </library> - </orderEntry> - </component> - <component name="ModuleRootManager"/> -</module> diff --git a/Grasscutter.ipr b/Grasscutter.ipr deleted file mode 100644 index 3759484a6..000000000 --- a/Grasscutter.ipr +++ /dev/null @@ -1,104 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="CompilerConfiguration"> - <option name="DEFAULT_COMPILER" value="Javac"/> - <resourceExtensions> - <entry name=".+\.(properties|xml|html|dtd|tld)"/> - <entry name=".+\.(gif|png|jpeg|jpg)"/> - </resourceExtensions> - <wildcardResourcePatterns> - <entry name="!?*.class"/> - <entry name="!?*.scala"/> - <entry name="!?*.groovy"/> - <entry name="!?*.java"/> - </wildcardResourcePatterns> - <annotationProcessing enabled="false" useClasspath="true"/> - <bytecodeTargetLevel target="17"/> - </component> - <component name="CopyrightManager" default=""> - <module2copyright/> - </component> - <component name="DependencyValidationManager"> - <option name="SKIP_IMPORT_STATEMENTS" value="false"/> - </component> - <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"/> - <component name="GradleUISettings"> - <setting name="root"/> - </component> - <component name="GradleUISettings2"> - <setting name="root"/> - </component> - <component name="IdProvider" IDEtalkID="11DA1DB66DD62DDA1ED602B7079FE97C"/> - <component name="JavadocGenerationManager"> - <option name="OUTPUT_DIRECTORY"/> - <option name="OPTION_SCOPE" value="protected"/> - <option name="OPTION_HIERARCHY" value="true"/> - <option name="OPTION_NAVIGATOR" value="true"/> - <option name="OPTION_INDEX" value="true"/> - <option name="OPTION_SEPARATE_INDEX" value="true"/> - <option name="OPTION_DOCUMENT_TAG_USE" value="false"/> - <option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false"/> - <option name="OPTION_DOCUMENT_TAG_VERSION" value="false"/> - <option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true"/> - <option name="OPTION_DEPRECATED_LIST" value="true"/> - <option name="OTHER_OPTIONS" value=""/> - <option name="HEAP_SIZE"/> - <option name="LOCALE"/> - <option name="OPEN_IN_BROWSER" value="true"/> - </component> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/Grasscutter.iml" filepath="$PROJECT_DIR$/Grasscutter.iml"/> - </modules> - </component> - <component name="ProjectRootManager" version="2" languageLevel="JDK_17" assert-keyword="true" jdk-15="true" project-jdk-type="JavaSDK" assert-jdk-15="true" project-jdk-name="17"> - <output url="file://$PROJECT_DIR$/out"/> - </component> - <component name="SvnBranchConfigurationManager"> - <option name="mySupportsUserInfoFilter" value="true"/> - </component> - <component name="VcsDirectoryMappings"> - <mapping directory="" vcs=""/> - </component> - <component name="masterDetails"> - <states> - <state key="ArtifactsStructureConfigurable.UI"> - <UIState> - <splitter-proportions> - <SplitterProportionsDataImpl/> - </splitter-proportions> - <settings/> - </UIState> - </state> - <state key="Copyright.UI"> - <UIState> - <splitter-proportions> - <SplitterProportionsDataImpl/> - </splitter-proportions> - </UIState> - </state> - <state key="ProjectJDKs.UI"> - <UIState> - <splitter-proportions> - <SplitterProportionsDataImpl> - <option name="proportions"> - <list> - <option value="0.2"/> - </list> - </option> - </SplitterProportionsDataImpl> - </splitter-proportions> - <last-edited>1.6</last-edited> - </UIState> - </state> - <state key="ScopeChooserConfigurable.UI"> - <UIState> - <splitter-proportions> - <SplitterProportionsDataImpl/> - </splitter-proportions> - <settings/> - </UIState> - </state> - </states> - </component> -</project> diff --git a/Grasscutter.iws b/Grasscutter.iws deleted file mode 100644 index d5bc7591f..000000000 --- a/Grasscutter.iws +++ /dev/null @@ -1,207 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ChangeListManager"> - <option name="TRACKING_ENABLED" value="true"/> - <option name="SHOW_DIALOG" value="false"/> - <option name="HIGHLIGHT_CONFLICTS" value="true"/> - <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false"/> - <option name="LAST_RESOLUTION" value="IGNORE"/> - </component> - <component name="ChangesViewManager" flattened_view="true" show_ignored="false"/> - <component name="CreatePatchCommitExecutor"> - <option name="PATCH_PATH" value=""/> - <option name="REVERSE_PATCH" value="false"/> - </component> - <component name="DaemonCodeAnalyzer"> - <disable_hints/> - </component> - <component name="DebuggerManager"> - <breakpoint_any> - <breakpoint> - <option name="NOTIFY_CAUGHT" value="true"/> - <option name="NOTIFY_UNCAUGHT" value="true"/> - <option name="ENABLED" value="false"/> - <option name="LOG_ENABLED" value="false"/> - <option name="LOG_EXPRESSION_ENABLED" value="false"/> - <option name="SUSPEND_POLICY" value="SuspendAll"/> - <option name="COUNT_FILTER_ENABLED" value="false"/> - <option name="COUNT_FILTER" value="0"/> - <option name="CONDITION_ENABLED" value="false"/> - <option name="CLASS_FILTERS_ENABLED" value="false"/> - <option name="INSTANCE_FILTERS_ENABLED" value="false"/> - <option name="CONDITION" value=""/> - <option name="LOG_MESSAGE" value=""/> - </breakpoint> - <breakpoint> - <option name="NOTIFY_CAUGHT" value="true"/> - <option name="NOTIFY_UNCAUGHT" value="true"/> - <option name="ENABLED" value="false"/> - <option name="LOG_ENABLED" value="false"/> - <option name="LOG_EXPRESSION_ENABLED" value="false"/> - <option name="SUSPEND_POLICY" value="SuspendAll"/> - <option name="COUNT_FILTER_ENABLED" value="false"/> - <option name="COUNT_FILTER" value="0"/> - <option name="CONDITION_ENABLED" value="false"/> - <option name="CLASS_FILTERS_ENABLED" value="false"/> - <option name="INSTANCE_FILTERS_ENABLED" value="false"/> - <option name="CONDITION" value=""/> - <option name="LOG_MESSAGE" value=""/> - </breakpoint> - </breakpoint_any> - <breakpoint_rules/> - <ui_properties/> - </component> - <component name="ModuleEditorState"> - <option name="LAST_EDITED_MODULE_NAME"/> - <option name="LAST_EDITED_TAB_NAME"/> - </component> - <component name="ProjectInspectionProfilesVisibleTreeState"> - <entry key="Project Default"> - <profile-state/> - </entry> - </component> - <component name="ProjectLevelVcsManager"> - <OptionsSetting value="true" id="Add"/> - <OptionsSetting value="true" id="Remove"/> - <OptionsSetting value="true" id="Checkout"/> - <OptionsSetting value="true" id="Update"/> - <OptionsSetting value="true" id="Status"/> - <OptionsSetting value="true" id="Edit"/> - <ConfirmationsSetting value="0" id="Add"/> - <ConfirmationsSetting value="0" id="Remove"/> - </component> - <component name="ProjectReloadState"> - <option name="STATE" value="0"/> - </component> - <component name="PropertiesComponent"> - <property name="GoToFile.includeJavaFiles" value="false"/> - <property name="GoToClass.toSaveIncludeLibraries" value="false"/> - <property name="MemberChooser.sorted" value="false"/> - <property name="MemberChooser.showClasses" value="true"/> - <property name="GoToClass.includeLibraries" value="false"/> - <property name="MemberChooser.copyJavadoc" value="false"/> - </component> - <component name="RunManager"> - <configuration default="true" type="Remote" factoryName="Remote"> - <option name="USE_SOCKET_TRANSPORT" value="true"/> - <option name="SERVER_MODE" value="false"/> - <option name="SHMEM_ADDRESS" value="javadebug"/> - <option name="HOST" value="localhost"/> - <option name="PORT" value="5005"/> - <method> - <option name="BuildArtifacts" enabled="false"/> - </method> - </configuration> - <configuration default="true" type="Applet" factoryName="Applet"> - <module name=""/> - <option name="MAIN_CLASS_NAME"/> - <option name="HTML_FILE_NAME"/> - <option name="HTML_USED" value="false"/> - <option name="WIDTH" value="400"/> - <option name="HEIGHT" value="300"/> - <option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy"/> - <option name="VM_PARAMETERS"/> - <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> - <option name="ALTERNATIVE_JRE_PATH"/> - <method> - <option name="BuildArtifacts" enabled="false"/> - <option name="Make" enabled="true"/> - </method> - </configuration> - <configuration default="true" type="Application" factoryName="Application"> - <extension name="coverage" enabled="false" merge="false"/> - <option name="MAIN_CLASS_NAME"/> - <option name="VM_PARAMETERS"/> - <option name="PROGRAM_PARAMETERS"/> - <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/> - <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> - <option name="ALTERNATIVE_JRE_PATH"/> - <option name="ENABLE_SWING_INSPECTOR" value="false"/> - <option name="ENV_VARIABLES"/> - <option name="PASS_PARENT_ENVS" value="true"/> - <module name=""/> - <envs/> - <method> - <option name="BuildArtifacts" enabled="false"/> - <option name="Make" enabled="true"/> - </method> - </configuration> - <configuration default="true" type="JUnit" factoryName="JUnit"> - <extension name="coverage" enabled="false" merge="false"/> - <module name=""/> - <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false"/> - <option name="ALTERNATIVE_JRE_PATH"/> - <option name="PACKAGE_NAME"/> - <option name="MAIN_CLASS_NAME"/> - <option name="METHOD_NAME"/> - <option name="TEST_OBJECT" value="class"/> - <option name="VM_PARAMETERS"/> - <option name="PARAMETERS"/> - <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/> - <option name="ENV_VARIABLES"/> - <option name="PASS_PARENT_ENVS" value="true"/> - <option name="TEST_SEARCH_SCOPE"> - <value defaultName="moduleWithDependencies"/> - </option> - <envs/> - <method> - <option name="BuildArtifacts" enabled="false"/> - <option name="Make" enabled="true"/> - </method> - </configuration> - <list size="0"/> - <configuration name="<template>" type="WebApp" default="true" selected="false"> - <Host>localhost</Host> - <Port>5050</Port> - </configuration> - </component> - <component name="ShelveChangesManager" show_recycled="false"/> - <component name="SvnConfiguration" maxAnnotateRevisions="500"> - <option name="USER" value=""/> - <option name="PASSWORD" value=""/> - <option name="LAST_MERGED_REVISION"/> - <option name="UPDATE_RUN_STATUS" value="false"/> - <option name="MERGE_DRY_RUN" value="false"/> - <option name="MERGE_DIFF_USE_ANCESTRY" value="true"/> - <option name="UPDATE_LOCK_ON_DEMAND" value="false"/> - <option name="IGNORE_SPACES_IN_MERGE" value="false"/> - <option name="DETECT_NESTED_COPIES" value="true"/> - <option name="IGNORE_SPACES_IN_ANNOTATE" value="true"/> - <option name="SHOW_MERGE_SOURCES_IN_ANNOTATE" value="true"/> - <myIsUseDefaultProxy>false</myIsUseDefaultProxy> - </component> - <component name="TaskManager"> - <task active="true" id="Default" summary="Default task"/> - <servers/> - </component> - <component name="VcsManagerConfiguration"> - <option name="OFFER_MOVE_TO_ANOTHER_CHANGELIST_ON_PARTIAL_COMMIT" value="true"/> - <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="true"/> - <option name="PERFORM_UPDATE_IN_BACKGROUND" value="true"/> - <option name="PERFORM_COMMIT_IN_BACKGROUND" value="true"/> - <option name="PERFORM_EDIT_IN_BACKGROUND" value="true"/> - <option name="PERFORM_CHECKOUT_IN_BACKGROUND" value="true"/> - <option name="PERFORM_ADD_REMOVE_IN_BACKGROUND" value="true"/> - <option name="PERFORM_ROLLBACK_IN_BACKGROUND" value="false"/> - <option name="CHECK_LOCALLY_CHANGED_CONFLICTS_IN_BACKGROUND" value="false"/> - <option name="ENABLE_BACKGROUND_PROCESSES" value="false"/> - <option name="CHANGED_ON_SERVER_INTERVAL" value="60"/> - <option name="FORCE_NON_EMPTY_COMMENT" value="false"/> - <option name="LAST_COMMIT_MESSAGE"/> - <option name="MAKE_NEW_CHANGELIST_ACTIVE" value="true"/> - <option name="OPTIMIZE_IMPORTS_BEFORE_PROJECT_COMMIT" value="false"/> - <option name="CHECK_FILES_UP_TO_DATE_BEFORE_COMMIT" value="false"/> - <option name="REFORMAT_BEFORE_PROJECT_COMMIT" value="false"/> - <option name="REFORMAT_BEFORE_FILE_COMMIT" value="false"/> - <option name="FILE_HISTORY_DIALOG_COMMENTS_SPLITTER_PROPORTION" value="0.8"/> - <option name="FILE_HISTORY_DIALOG_SPLITTER_PROPORTION" value="0.5"/> - <option name="ACTIVE_VCS_NAME"/> - <option name="UPDATE_GROUP_BY_PACKAGES" value="false"/> - <option name="UPDATE_GROUP_BY_CHANGELIST" value="false"/> - <option name="SHOW_FILE_HISTORY_AS_TREE" value="false"/> - <option name="FILE_HISTORY_SPLITTER_PROPORTION" value="0.6"/> - </component> - <component name="XDebuggerManager"> - <breakpoint-manager/> - </component> -</project> diff --git a/grasscutter-1.0.3-dev.jar.asc b/grasscutter-1.0.3-dev.jar.asc deleted file mode 100644 index 0249d0297..000000000 --- a/grasscutter-1.0.3-dev.jar.asc +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PGP SIGNATURE----- -Version: BCPG v1.68 - -iQGcBAABCgAGBQJicbItAAoJEK1DoRSUkxpGzZgMAJaxuuXmG3V1gFdJLXoKR+6s -moOjwyD8UDfFModX92Thccgox9/j72A1x8sKQfJNDzI5wx51iR2rXw52KE6GhdVI -vSyhnGv6LGOtGtA59i8wnXcEKBD33Qm6B2KFD4ox4JEheMb/wWK3zF09aknLkUVX -43L48E4dF0lAzJ7QWZTTNKCK156Bwa3F8NhVLGGD6tpGahkS8J73Ax6C8uu6zVKf -8dftBpI+0YxPJkbxoPahVZVmFawUjcjPDcRwG5LTO6McVUI9YzczSHdk0FG39ENo -HXvwsK/xnN2Xy8ta+ylu9Eta0zx9mLTZjEjUQ3B8XjTDDVcz11DlvE5L1jJ8Gov+ -XbCM0m+od0hziCwuYg2BOsi13C9vejA5BoCHeejNTy+QiGGhK4QdyxdufxQD1Bo4 -uF8ZmmeC1AMA7m1y4tqIqwA5iJQx4KaB3aKw8np0bYuVVNnw75wpf3NBcQKW/Jf7 -diKjcimqhSkPpJ/ok0ZqITpMTaYhZoXnyUFsm0DIHA== -=5WgW ------END PGP SIGNATURE----- From 82a88c85738bae1ff5f2e4c1f8b7a4ae5cc93a65 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 15:43:51 -0700 Subject: [PATCH 144/434] fix/runningAndDashingStamina --- .../MovementManager/MovementManager.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index ed1645936..18958f355 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -268,7 +268,7 @@ public class MovementManager { if (Grasscutter.getConfig().OpenStamina) { boolean moving = isPlayerMoving(); if (moving || (getCurrentStamina() < getMaximumStamina())) { - Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); + // Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); Consumption consumption = Consumption.None; // TODO: refactor these conditions. @@ -306,14 +306,16 @@ public class MovementManager { } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { // RUN, DASH and WALK // DASH - if (currentState == MotionState.MOTION_DASH) { - if (previousState == MotionState.MOTION_DASH) { + if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) { + consumption = Consumption.DASH; + if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) { + // only charge once consumption = Consumption.SPRINT; - } else { - // cost more to start dashing - consumption = Consumption.DASH; } } + if (currentState == MotionState.MOTION_DASH) { + consumption = Consumption.SPRINT; + } // RUN if (currentState == MotionState.MOTION_RUN) { consumption = Consumption.RUN; @@ -347,14 +349,13 @@ public class MovementManager { staminaRecoverDelay = 0; } if (consumption.amount > 0) { - if (staminaRecoverDelay < 5) { + if (staminaRecoverDelay < 10) { staminaRecoverDelay++; consumption = Consumption.None; } } int newStamina = updateStamina(cachedSession, consumption.amount); cachedSession.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t" + consumption + "(" + consumption.amount + ")"); } } From 4264ca04859c1b15189ea5a59dc2a8c1a09474fb Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 19:47:30 -0400 Subject: [PATCH 145/434] Update `Utils.java` --- .../java/emu/grasscutter/utils/Utils.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 268cc6f40..a72815f58 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -5,6 +5,8 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.*; import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.Map; import java.util.Random; import emu.grasscutter.Config; @@ -260,4 +262,41 @@ public final class Utils { Grasscutter.getLogger().warn("Failed to read from input stream."); } return stringBuilder.toString(); } + + /** + * Switch properties from upper case to lower case? + */ + public static Map<String, Object> switchPropertiesUpperLowerCase(Map<String, Object> objMap, Class<?> cls) { + Map<String, Object> map = new HashMap<>(objMap.size()); + for (String key : objMap.keySet()) { + try { + char c = key.charAt(0); + if (c >= 'a' && c <= 'z') { + try { + cls.getDeclaredField(key); + map.put(key, objMap.get(key)); + } catch (NoSuchFieldException e) { + String s1 = String.valueOf(c).toUpperCase(); + String after = key.length() > 1 ? s1 + key.substring(1) : s1; + cls.getDeclaredField(after); + map.put(after, objMap.get(key)); + } + } else if (c >= 'A' && c <= 'Z') { + try { + cls.getDeclaredField(key); + map.put(key, objMap.get(key)); + } catch (NoSuchFieldException e) { + String s1 = String.valueOf(c).toLowerCase(); + String after = key.length() > 1 ? s1 + key.substring(1) : s1; + cls.getDeclaredField(after); + map.put(after, objMap.get(key)); + } + } + } catch (NoSuchFieldException e) { + map.put(key, objMap.get(key)); + } + } + + return map; + } } From c5bf5ecd69cec7465884570782fa2b4061793484 Mon Sep 17 00:00:00 2001 From: Bwly999 <438225686@qq.com> Date: Sat, 7 May 2022 08:00:31 +0800 Subject: [PATCH 146/434] roll back to timer --- src/main/java/emu/grasscutter/server/game/GameServer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 0135145c0..e6709abf5 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -205,7 +205,7 @@ public final class GameServer extends KcpServer { @Override public synchronized void start() { // Schedule game loop. - ScheduledExecutorService gameLoop = Executors.newScheduledThreadPool(2); + Timer gameLoop = new Timer(); gameLoop.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -215,7 +215,8 @@ public final class GameServer extends KcpServer { Grasscutter.getLogger().error(Grasscutter.getLanguage().An_error_occurred_during_game_update, e); } } - }, 0L, 1000L, TimeUnit.MILLISECONDS); + }, new Date(), 1000L); + super.start(); } From 59b24aa481c6e2747d6899d07c1f201a6bcbc0be Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 20:18:50 -0400 Subject: [PATCH 147/434] Rename `en-US` locale --- src/main/resources/languages/en-US.json | 298 ++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/main/resources/languages/en-US.json diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json new file mode 100644 index 000000000..48ffb5b0b --- /dev/null +++ b/src/main/resources/languages/en-US.json @@ -0,0 +1,298 @@ +{ + "messages": { + "game": { + "port_bind": "Game Server started on port %s", + "connect": "Client connected from %s", + "disconnect": "Client disconnected from %s", + "game_update_error": "An error occurred during game update.", + "command_error": "Command error:" + }, + "dispatch": { + "port_bind": "[Dispatch] Dispatch server started on port %s", + "request": "[Dispatch] Client %s %s request: %s", + "keystore": { + "general_error": "[Dispatch] Error while loading keystore!", + "password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...", + "no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.", + "default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json." + }, + "no_commands_error": "Commands are not supported in dispatch only mode.", + "unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s", + "account": { + "login_attempt": "[Dispatch] Client %s is trying to log in", + "login_success": "[Dispatch] Client %s logged in as %s", + "login_token_attempt": "[Dispatch] Client %s is trying to log in via token", + "login_token_error": "[Dispatch] Client %s failed to log in via token", + "login_token_success": "[Dispatch] Client %s logged in via token as %s", + "combo_token_success": "[Dispatch] Client %s succeed to exchange combo token", + "combo_token_error": "[Dispatch] Client %s failed to exchange combo token", + "account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created", + "account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed", + "account_login_exist_error": "[Dispatch] Client %s failed to log in: Account no found", + "account_cache_error": "Game account cache information error", + "session_key_error": "Wrong session key.", + "username_error": "Username not found.", + "username_create_error": "Username not found, create failed." + } + }, + "status": { + "free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter", + "starting": "Starting Grasscutter...", + "shutdown": "Shutting down...", + "done": "Done! For help, type \"help\"", + "error": "An error occurred.", + "welcome": "Welcome to Grasscutter", + "run_mode_error": "Invalid server run mode: %s.", + "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", + "create_resources": "Creating resources folder...", + "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder." + } + }, + "commands": { + "generic": { + "not_specified": "No command specified.", + "unknown_command": "Unknown command: %s", + "permission_error": "You do not have permission to run this command.", + "console_execute_error": "This command can only be run from the console.", + "player_execute_error": "Run this command in-game.", + "command_exist_error": "No command found.", + "invalid": { + "amount": "Invalid amount.", + "artifactId": "Invalid artifactId.", + "avatarId": "Invalid avatarId.", + "avatarLevel": "Invalid avatarLevel.", + "entityId": "Invalid entityId.", + "itemId": "Invalid itemId.", + "itemLevel": "Invalid itemLevel.", + "itemRefinement": "Invalid itemRefinement.", + "playerId": "Invalid playerId.", + "uid": "Invalid UID." + } + }, + "execution": { + "uid_error": "Invalid UID.", + "player_exist_error": "Player not found.", + "player_offline_error": "Player is not online.", + "item_id_error": "Invalid item ID.", + "item_player_exist_error": "Invalid item or UID.", + "entity_id_error": "Invalid entity ID.", + "player_exist_offline_error": "Player not found or is not online.", + "argument_error": "Invalid arguments.", + "clear_target": "Target cleared.", + "set_target": "Subsequent commands will target @%s by default.", + "need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID." + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "help": "Help", + "success": "Success" + }, + "account": { + "modify": "Modify user accounts", + "invalid": "Invalid UID.", + "exists": "Account already exists.", + "create": "Account created with UID %s.", + "delete": "Account deleted.", + "no_account": "Account not found.", + "command_usage": "Usage: account <create|delete> <username> [uid]" + }, + "broadcast": { + "command_usage": "Usage: broadcast <message>", + "message_sent": "Message sent." + }, + "changescene": { + "usage": "Usage: changescene <sceneId>", + "already_in_scene": "You are already in that scene.", + "success": "Changed to scene %s.", + "exists_error": "The specified scene does not exist." + }, + "clear": { + "command_usage": "Usage: clear <all|wp|art|mat>", + "weapons": "Cleared weapons for %s.", + "artifacts": "Cleared artifacts for %s.", + "materials": "Cleared materials for %s.", + "furniture": "Cleared furniture for %s.", + "displays": "Cleared displays for %s.", + "virtuals": "Cleared virtuals for %s.", + "everything": "Cleared everything for %s." + }, + "coop": { + "usage": "Usage: coop <playerId> <target playerId>", + "success": "Summoned %s to %s's world." + }, + "enter_dungeon": { + "usage": "Usage: enterdungeon <dungeon id>", + "changed": "Changed to dungeon %s", + "not_found_error": "Dungeon does not exist", + "in_dungeon_error": "You are already in that dungeon" + }, + "giveAll": { + "usage": "Usage: giveall [player] [amount]", + "started": "Receiving all items...", + "success": "Successfully gave all items to %s.", + "invalid_amount_or_playerId": "Invalid amount or player ID." + }, + "giveArtifact": { + "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "id_error": "Invalid artifact ID.", + "success": "Given %s to %s." + }, + "giveChar": { + "usage": "Usage: givechar <player> <itemId|itemName> [amount]", + "given": "Given %s with level %s to %s.", + "invalid_avatar_id": "Invalid avatar id.", + "invalid_avatar_level": "Invalid avatar level.", + "invalid_avatar_or_player_id": "Invalid avatar or player ID." + }, + "give": { + "usage": "Usage: give <player> <itemId|itemName> [amount] [level]", + "refinement_only_applicable_weapons": "Refinement is only applicable to weapons.", + "refinement_must_between_1_and_5": "Refinement must be between 1 and 5.", + "given": "Given %s of %s to %s.", + "given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s", + "given_level": "Given %s with level %s %s times to %s" + }, + "godmode": { + "success": "Godmode is now %s for %s." + }, + "heal": { + "success": "All characters have been healed." + }, + "kick": { + "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", + "server_kick_player": "Kicking player [%s:%s]" + }, + "kill": { + "usage": "Usage: killall [playerUid] [sceneId]", + "scene_not_found_in_player_world": "Scene not found in player world", + "kill_monsters_in_scene": "Killing %s monsters in scene %s" + }, + "killCharacter": { + "usage": "Usage: /killcharacter [playerId]", + "success": "Killed %s's current character." + }, + "list": { + "success": "There are %s player(s) online:" + }, + "permission": { + "usage": "Usage: permission <add|remove> <username> <permission>", + "add": "Permission added.", + "has_error": "They already have this permission!", + "remove": "Permission removed.", + "not_have_error": "They don't have this permission!", + "account_error": "The account cannot be found." + }, + "position": { + "success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" + }, + "reload": { + "reload_start": "Reloading config.", + "reload_done": "Reload complete." + }, + "resetConst": { + "reset_all": "Reset all avatars' constellations.", + "success": "Constellations for %s have been reset. Please relog to see changes." + }, + "resetShopLimit": { + "usage": "Usage: /resetshop <player id>" + }, + "sendMail": { + "usage": "Usage: give [player] <itemId|itemName> [amount]", + "user_not_exist": "The user with an id of '%s' does not exist", + "start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time", + "templates": "Mail templates coming soon implemented...", + "invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`", + "send_cancel": "Message sending cancelled", + "send_done": "Message sent to user %s!", + "send_all_done": "Message sent to all users!", + "not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel", + "please_use": "Please use `/sendmail %s`", + "set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.", + "set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.", + "set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.", + "send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.", + "invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`", + "title": "<title>", + "message": "<message>", + "sender": "<sender>", + "arguments": "<itemId|itemName|finish> [amount] [level]", + "error": "ERROR: invalid construction stage %s. Check console for stacktrace." + }, + "sendMessage": { + "usage": "Usage: sendmessage <player> <message>", + "success": "Message sent." + }, + "setFetterLevel": { + "usage": "Usage: setfetterlevel <level>", + "range_error": "Fetter level must be between 0 and 10.", + "success": "Fetter level set to %s", + "level_error": "Invalid fetter level." + }, + "setStats": { + "usage_console": "Usage: setstats|stats @<UID> <stat> <value>", + "usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>", + "help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n", + "value_error": "Invalid stat value.", + "uid_error": "Invalid UID.", + "player_error": "Player not found or offline.", + "set_self": "%s set to %s.", + "set_for_uid": "%s for %s set to %s.", + "set_max_hp": "MAX HP set to %s." + }, + "setWorldLevel": { + "usage": "Usage: setworldlevel <level>", + "value_error": "World level must be between 0-8", + "success": "World level set to %s.", + "invalid_world_level": "Invalid world level." + }, + "spawn": { + "usage": "Usage: spawn <entityId> [amount] [level(monster only)]", + "success": "Spawned %s of %s." + }, + "stop": { + "success": "Server shutting down..." + }, + "talent": { + "usage_1": "To set talent level: /talent set <talentID> <value>", + "usage_2": "Another way to set talent level: /talent <n or e or q> <value>", + "usage_3": "To get talent ID: /talent getid", + "lower_16": "Invalid talent level. Level should be lower than 16", + "set_id": "Set talent to %s.", + "set_atk": "Set talent Normal ATK to %s.", + "set_e": "Set talent E to %s.", + "set_q": "Set talent Q to %s.", + "invalid_skill_id": "Invalid skill ID.", + "set_this": "Set this talent to %s.", + "invalid_level": "Invalid talent level.", + "normal_attack_id": "Normal Attack ID %s.", + "e_skill_id": "E skill ID %s.", + "q_skill_id": "Q skill ID %s." + }, + "teleportAll": { + "success": "Summoned all players to your location.", + "error": "You only can use this command in MP mode." + }, + "teleport": { + "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", + "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", + "specify_player_id": "You must specify a player id.", + "invalid_position": "Invalid position.", + "success": "Teleported %s to %s, %s, %s in scene %s" + }, + "weather": { + "usage": "Usage: weather <weatherId> [climateId]", + "success": "Changed weather to %s with climate %s", + "invalid_id": "Invalid ID." + }, + "drop": { + "command_usage": "Usage: drop <itemId|itemName> [amount]", + "success": "Dropped %s of %s." + }, + "help": { + "usage": "Usage: ", + "aliases": "Aliases: ", + "available_commands": "Available commands: " + } + } +} \ No newline at end of file From e6cb97a4372fa92304d89f25c3433efbd6691b6a Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 21:04:39 -0400 Subject: [PATCH 148/434] Bug fixes --- .../java/emu/grasscutter/Grasscutter.java | 14 +- .../server/game/GameServerPacketHandler.java | 1 + .../java/emu/grasscutter/tools/Tools.java | 2 - .../java/emu/grasscutter/utils/Language.java | 2 +- src/main/resources/languages/en_US.json | 298 ------------------ 5 files changed, 13 insertions(+), 304 deletions(-) delete mode 100644 src/main/resources/languages/en_US.json diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 12f37b0fa..463823ed9 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -70,12 +70,20 @@ public final class Grasscutter { Crypto.loadKeys(); // Load keys from buffers. // Parse arguments. + boolean exitEarly = false; for (String arg : args) { switch (arg.toLowerCase()) { - case "-handbook" -> Tools.createGmHandbook(); - case "-gachamap" -> Tools.createGachaMapping("./gacha-mapping.js"); + case "-handbook" -> { + Tools.createGmHandbook(); exitEarly = true; + } + case "-gachamap" -> { + Tools.createGachaMapping("./gacha-mapping.js"); exitEarly = true; + } } - } + } + + // Exit early if argument sets it. + if(exitEarly) System.exit(0); // Initialize server. Grasscutter.getLogger().info(translate("messages.status.starting")); diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 35303b639..88e7fa17f 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -14,6 +14,7 @@ import emu.grasscutter.server.game.GameSession.SessionState; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +@SuppressWarnings("unchecked") public class GameServerPacketHandler { private final Int2ObjectMap<PacketHandler> handlers; diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 59c264b2b..475649b1f 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -93,7 +93,6 @@ public final class Tools { } Grasscutter.getLogger().info("GM Handbook generated!"); - System.exit(0); } @SuppressWarnings("deprecation") @@ -183,6 +182,5 @@ public final class Tools { } Grasscutter.getLogger().info("Mappings generated!"); - System.exit(0); } } diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index c4af257e8..ec56b41d9 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -40,7 +40,7 @@ public final class Language { @Nullable JsonObject languageData = null; try { - InputStream file = Grasscutter.class.getResourceAsStream("/lang/" + fileName); + InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); } catch (Exception exception) { Grasscutter.getLogger().error("Failed to load language file: " + fileName, exception); diff --git a/src/main/resources/languages/en_US.json b/src/main/resources/languages/en_US.json deleted file mode 100644 index 48ffb5b0b..000000000 --- a/src/main/resources/languages/en_US.json +++ /dev/null @@ -1,298 +0,0 @@ -{ - "messages": { - "game": { - "port_bind": "Game Server started on port %s", - "connect": "Client connected from %s", - "disconnect": "Client disconnected from %s", - "game_update_error": "An error occurred during game update.", - "command_error": "Command error:" - }, - "dispatch": { - "port_bind": "[Dispatch] Dispatch server started on port %s", - "request": "[Dispatch] Client %s %s request: %s", - "keystore": { - "general_error": "[Dispatch] Error while loading keystore!", - "password_error": "[Dispatch] Unable to load keystore. Trying default keystore password...", - "no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.", - "default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json." - }, - "no_commands_error": "Commands are not supported in dispatch only mode.", - "unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s", - "account": { - "login_attempt": "[Dispatch] Client %s is trying to log in", - "login_success": "[Dispatch] Client %s logged in as %s", - "login_token_attempt": "[Dispatch] Client %s is trying to log in via token", - "login_token_error": "[Dispatch] Client %s failed to log in via token", - "login_token_success": "[Dispatch] Client %s logged in via token as %s", - "combo_token_success": "[Dispatch] Client %s succeed to exchange combo token", - "combo_token_error": "[Dispatch] Client %s failed to exchange combo token", - "account_login_create_success": "[Dispatch] Client %s failed to log in: Account %s created", - "account_login_create_error": "[Dispatch] Client %s failed to log in: Account create failed", - "account_login_exist_error": "[Dispatch] Client %s failed to log in: Account no found", - "account_cache_error": "Game account cache information error", - "session_key_error": "Wrong session key.", - "username_error": "Username not found.", - "username_create_error": "Username not found, create failed." - } - }, - "status": { - "free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter", - "starting": "Starting Grasscutter...", - "shutdown": "Shutting down...", - "done": "Done! For help, type \"help\"", - "error": "An error occurred.", - "welcome": "Welcome to Grasscutter", - "run_mode_error": "Invalid server run mode: %s.", - "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", - "create_resources": "Creating resources folder...", - "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder." - } - }, - "commands": { - "generic": { - "not_specified": "No command specified.", - "unknown_command": "Unknown command: %s", - "permission_error": "You do not have permission to run this command.", - "console_execute_error": "This command can only be run from the console.", - "player_execute_error": "Run this command in-game.", - "command_exist_error": "No command found.", - "invalid": { - "amount": "Invalid amount.", - "artifactId": "Invalid artifactId.", - "avatarId": "Invalid avatarId.", - "avatarLevel": "Invalid avatarLevel.", - "entityId": "Invalid entityId.", - "itemId": "Invalid itemId.", - "itemLevel": "Invalid itemLevel.", - "itemRefinement": "Invalid itemRefinement.", - "playerId": "Invalid playerId.", - "uid": "Invalid UID." - } - }, - "execution": { - "uid_error": "Invalid UID.", - "player_exist_error": "Player not found.", - "player_offline_error": "Player is not online.", - "item_id_error": "Invalid item ID.", - "item_player_exist_error": "Invalid item or UID.", - "entity_id_error": "Invalid entity ID.", - "player_exist_offline_error": "Player not found or is not online.", - "argument_error": "Invalid arguments.", - "clear_target": "Target cleared.", - "set_target": "Subsequent commands will target @%s by default.", - "need_target": "This command requires a target UID. Add a <@UID> argument or set a persistent target with /target @UID." - }, - "status": { - "enabled": "Enabled", - "disabled": "Disabled", - "help": "Help", - "success": "Success" - }, - "account": { - "modify": "Modify user accounts", - "invalid": "Invalid UID.", - "exists": "Account already exists.", - "create": "Account created with UID %s.", - "delete": "Account deleted.", - "no_account": "Account not found.", - "command_usage": "Usage: account <create|delete> <username> [uid]" - }, - "broadcast": { - "command_usage": "Usage: broadcast <message>", - "message_sent": "Message sent." - }, - "changescene": { - "usage": "Usage: changescene <sceneId>", - "already_in_scene": "You are already in that scene.", - "success": "Changed to scene %s.", - "exists_error": "The specified scene does not exist." - }, - "clear": { - "command_usage": "Usage: clear <all|wp|art|mat>", - "weapons": "Cleared weapons for %s.", - "artifacts": "Cleared artifacts for %s.", - "materials": "Cleared materials for %s.", - "furniture": "Cleared furniture for %s.", - "displays": "Cleared displays for %s.", - "virtuals": "Cleared virtuals for %s.", - "everything": "Cleared everything for %s." - }, - "coop": { - "usage": "Usage: coop <playerId> <target playerId>", - "success": "Summoned %s to %s's world." - }, - "enter_dungeon": { - "usage": "Usage: enterdungeon <dungeon id>", - "changed": "Changed to dungeon %s", - "not_found_error": "Dungeon does not exist", - "in_dungeon_error": "You are already in that dungeon" - }, - "giveAll": { - "usage": "Usage: giveall [player] [amount]", - "started": "Receiving all items...", - "success": "Successfully gave all items to %s.", - "invalid_amount_or_playerId": "Invalid amount or player ID." - }, - "giveArtifact": { - "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", - "id_error": "Invalid artifact ID.", - "success": "Given %s to %s." - }, - "giveChar": { - "usage": "Usage: givechar <player> <itemId|itemName> [amount]", - "given": "Given %s with level %s to %s.", - "invalid_avatar_id": "Invalid avatar id.", - "invalid_avatar_level": "Invalid avatar level.", - "invalid_avatar_or_player_id": "Invalid avatar or player ID." - }, - "give": { - "usage": "Usage: give <player> <itemId|itemName> [amount] [level]", - "refinement_only_applicable_weapons": "Refinement is only applicable to weapons.", - "refinement_must_between_1_and_5": "Refinement must be between 1 and 5.", - "given": "Given %s of %s to %s.", - "given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s", - "given_level": "Given %s with level %s %s times to %s" - }, - "godmode": { - "success": "Godmode is now %s for %s." - }, - "heal": { - "success": "All characters have been healed." - }, - "kick": { - "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", - "server_kick_player": "Kicking player [%s:%s]" - }, - "kill": { - "usage": "Usage: killall [playerUid] [sceneId]", - "scene_not_found_in_player_world": "Scene not found in player world", - "kill_monsters_in_scene": "Killing %s monsters in scene %s" - }, - "killCharacter": { - "usage": "Usage: /killcharacter [playerId]", - "success": "Killed %s's current character." - }, - "list": { - "success": "There are %s player(s) online:" - }, - "permission": { - "usage": "Usage: permission <add|remove> <username> <permission>", - "add": "Permission added.", - "has_error": "They already have this permission!", - "remove": "Permission removed.", - "not_have_error": "They don't have this permission!", - "account_error": "The account cannot be found." - }, - "position": { - "success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" - }, - "reload": { - "reload_start": "Reloading config.", - "reload_done": "Reload complete." - }, - "resetConst": { - "reset_all": "Reset all avatars' constellations.", - "success": "Constellations for %s have been reset. Please relog to see changes." - }, - "resetShopLimit": { - "usage": "Usage: /resetshop <player id>" - }, - "sendMail": { - "usage": "Usage: give [player] <itemId|itemName> [amount]", - "user_not_exist": "The user with an id of '%s' does not exist", - "start_composition": "Starting composition of message.\nPlease use `/sendmail <title>` to continue.\nYou can use `/sendmail stop` at any time", - "templates": "Mail templates coming soon implemented...", - "invalid_arguments": "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`", - "send_cancel": "Message sending cancelled", - "send_done": "Message sent to user %s!", - "send_all_done": "Message sent to all users!", - "not_composition_end": "Message composition not at final stage.\nPlease use `/sendmail %s` or `/sendmail stop` to cancel", - "please_use": "Please use `/sendmail %s`", - "set_title": "Message title set as '%s'.\nUse '/sendmail <content>' to continue.", - "set_contents": "Message contents set as '%s'.\nUse '/sendmail <sender>' to continue.", - "set_message_sender": "Message sender set as '%s'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue.", - "send": "Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.", - "invalid_arguments_please_use": "Invalid arguments \n Please use `/sendmail %s`", - "title": "<title>", - "message": "<message>", - "sender": "<sender>", - "arguments": "<itemId|itemName|finish> [amount] [level]", - "error": "ERROR: invalid construction stage %s. Check console for stacktrace." - }, - "sendMessage": { - "usage": "Usage: sendmessage <player> <message>", - "success": "Message sent." - }, - "setFetterLevel": { - "usage": "Usage: setfetterlevel <level>", - "range_error": "Fetter level must be between 0 and 10.", - "success": "Fetter level set to %s", - "level_error": "Invalid fetter level." - }, - "setStats": { - "usage_console": "Usage: setstats|stats @<UID> <stat> <value>", - "usage_ingame": "Usage: setstats|stats [@UID] <stat> <value>", - "help_message": "\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n", - "value_error": "Invalid stat value.", - "uid_error": "Invalid UID.", - "player_error": "Player not found or offline.", - "set_self": "%s set to %s.", - "set_for_uid": "%s for %s set to %s.", - "set_max_hp": "MAX HP set to %s." - }, - "setWorldLevel": { - "usage": "Usage: setworldlevel <level>", - "value_error": "World level must be between 0-8", - "success": "World level set to %s.", - "invalid_world_level": "Invalid world level." - }, - "spawn": { - "usage": "Usage: spawn <entityId> [amount] [level(monster only)]", - "success": "Spawned %s of %s." - }, - "stop": { - "success": "Server shutting down..." - }, - "talent": { - "usage_1": "To set talent level: /talent set <talentID> <value>", - "usage_2": "Another way to set talent level: /talent <n or e or q> <value>", - "usage_3": "To get talent ID: /talent getid", - "lower_16": "Invalid talent level. Level should be lower than 16", - "set_id": "Set talent to %s.", - "set_atk": "Set talent Normal ATK to %s.", - "set_e": "Set talent E to %s.", - "set_q": "Set talent Q to %s.", - "invalid_skill_id": "Invalid skill ID.", - "set_this": "Set this talent to %s.", - "invalid_level": "Invalid talent level.", - "normal_attack_id": "Normal Attack ID %s.", - "e_skill_id": "E skill ID %s.", - "q_skill_id": "Q skill ID %s." - }, - "teleportAll": { - "success": "Summoned all players to your location.", - "error": "You only can use this command in MP mode." - }, - "teleport": { - "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", - "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", - "specify_player_id": "You must specify a player id.", - "invalid_position": "Invalid position.", - "success": "Teleported %s to %s, %s, %s in scene %s" - }, - "weather": { - "usage": "Usage: weather <weatherId> [climateId]", - "success": "Changed weather to %s with climate %s", - "invalid_id": "Invalid ID." - }, - "drop": { - "command_usage": "Usage: drop <itemId|itemName> [amount]", - "success": "Dropped %s of %s." - }, - "help": { - "usage": "Usage: ", - "aliases": "Aliases: ", - "available_commands": "Available commands: " - } - } -} \ No newline at end of file From 32b9e8f9d2676dad54054a46c9e74327f18afdc8 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 21:20:41 -0400 Subject: [PATCH 149/434] Rename & refactor --- src/main/resources/languages/{en_US.json => en-US.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/languages/{en_US.json => en-US.json} (100%) diff --git a/src/main/resources/languages/en_US.json b/src/main/resources/languages/en-US.json similarity index 100% rename from src/main/resources/languages/en_US.json rename to src/main/resources/languages/en-US.json From eb391b38335f2e229cc0a2b310872b8ccd7e7139 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Thu, 5 May 2022 21:56:34 -0700 Subject: [PATCH 150/434] Prompt for user language option for the `Tools` * GM Handbook * Gacha Map --- .../java/emu/grasscutter/tools/Tools.java | 84 +++++++++++++++++-- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 475649b1f..2081299d3 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -1,10 +1,12 @@ package emu.grasscutter.tools; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; +import java.io.FilenameFilter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; @@ -30,13 +32,73 @@ import emu.grasscutter.data.def.SceneData; import emu.grasscutter.utils.Utils; public final class Tools { - - @SuppressWarnings("deprecation") public static void createGmHandbook() throws Exception { + ToolsWithLanguageOption.createGmHandbook(getLanguageOption()); + } + + public static void createGachaMapping(String location) throws Exception { + ToolsWithLanguageOption.createGachaMapping(location, getLanguageOption()); + } + + public static List<String> getAvailableLanguage() throws Exception { + File textMapFolder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap"); + List<String> availableLangList = new ArrayList<String>(); + for (String textMapFileName : textMapFolder.list(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name.startsWith("TextMap") && name.endsWith(".json")){ + return true; + } + return false; + } + })) { + availableLangList.add(textMapFileName.replace("TextMap","").replace(".json","").toLowerCase()); + } + return availableLangList; + } + + public static String getLanguageOption() throws Exception { + List<String> availableLangList = getAvailableLanguage(); + + // Use system out for better format + if (availableLangList.size() == 1) { + return availableLangList.get(0).toUpperCase(); + } + System.out.println("The following languages mappings are available, please select one: [default: EN]"); + String groupedLangList = "> "; + int groupedLangCount = 0; + String input = ""; + for (String availableLanguage: availableLangList){ + groupedLangCount++; + groupedLangList = groupedLangList + "" + availableLanguage + "\t"; + if (groupedLangCount == 6) { + System.out.println(groupedLangList); + groupedLangCount = 0; + groupedLangList = "> "; + } + } + if (groupedLangCount > 0) { + System.out.println(groupedLangList); + } + System.out.print("\nYour choice:[EN]"); + + input = new BufferedReader(new InputStreamReader(System.in)).readLine(); + if (availableLangList.contains(input.toLowerCase())) { + return input.toUpperCase(); + } + Grasscutter.getLogger().info("Invalid option. Will use EN(English) as fallback"); + + return "EN"; + } +} + +final class ToolsWithLanguageOption { + @SuppressWarnings("deprecation") + public static void createGmHandbook(String language) throws Exception { ResourceLoader.loadResources(); Map<Long, String> map; - try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMapEN.json")), StandardCharsets.UTF_8)) { + try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) { map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType()); } @@ -96,11 +158,11 @@ public final class Tools { } @SuppressWarnings("deprecation") - public static void createGachaMapping(String location) throws Exception { + public static void createGachaMapping(String location, String language) throws Exception { ResourceLoader.loadResources(); Map<Long, String> map; - try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMapEN.json")), StandardCharsets.UTF_8)) { + try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) { map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType()); } @@ -113,6 +175,9 @@ public final class Tools { list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); Collections.sort(list); + // if the user made choices for language, I assume it's okay to assign his/her selected language to "en-us" + // since it's the fallback language and there will be no difference in the gacha record page. + // The enduser can still modify the `gacha_mappings.js` directly to enable multilingual for the gacha record system. writer.println("mappings = {\"en-us\": {"); // Avatars @@ -140,10 +205,10 @@ public final class Tools { default: color = "blue"; } - + // Got the magic number 4233146695 from manually search in the json file writer.println( "\"" + (avatarID % 1000 + 1000) + "\" : [\"" - + map.get(data.getNameTextMapHash()) + "(Avatar)\", \"" + + map.get(data.getNameTextMapHash()) + "(" + map.get(4233146695L)+ ")\", \"" + color + "\"]"); } @@ -173,9 +238,12 @@ public final class Tools { default: continue; // skip unnecessary entries } + + // Got the magic number 4231343903 from manually search in the json file + writer.println(",\"" + data.getId() + "\" : [\"" + map.get(data.getNameTextMapHash()).replaceAll("\"", "") - + "(Weapon)\",\""+ color + "\"]"); + + "("+ map.get(4231343903L)+")\",\""+ color + "\"]"); } writer.println(",\"200\": \"Standard\", \"301\": \"Avatar Event\", \"302\": \"Weapon event\""); writer.println("}\n}"); From 6c7b94c127706b1de1c2c60dfc346f2890ad7c13 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Fri, 6 May 2022 01:47:25 -0700 Subject: [PATCH 151/434] Auto fill name of the event from TextMap --- src/main/java/emu/grasscutter/tools/Tools.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 2081299d3..3cb1e07c2 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -245,7 +245,7 @@ final class ToolsWithLanguageOption { "\" : [\"" + map.get(data.getNameTextMapHash()).replaceAll("\"", "") + "("+ map.get(4231343903L)+")\",\""+ color + "\"]"); } - writer.println(",\"200\": \"Standard\", \"301\": \"Avatar Event\", \"302\": \"Weapon event\""); + writer.println(",\"200\": \""+map.get(332935371L)+"\", \"301\": \""+ map.get(2272170627L) + "\", \"302\": \""+map.get(2864268523L)+"\""); writer.println("}\n}"); } From c8779fec3ce605ee0d18e38efbb46406cdb8e68a Mon Sep 17 00:00:00 2001 From: Magix <kobedo11@gmail.com> Date: Fri, 6 May 2022 21:12:39 -0400 Subject: [PATCH 152/434] Update Tools.java --- src/main/java/emu/grasscutter/tools/Tools.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 3cb1e07c2..a2bee91f0 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -80,7 +80,7 @@ public final class Tools { if (groupedLangCount > 0) { System.out.println(groupedLangList); } - System.out.print("\nYour choice:[EN]"); + System.out.print("\nYour choice:[EN] "); input = new BufferedReader(new InputStreamReader(System.in)).readLine(); if (availableLangList.contains(input.toLowerCase())) { From a93371a0d0aa0c650c83abdd110f1f87b9095922 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Fri, 6 May 2022 21:52:10 -0400 Subject: [PATCH 153/434] Fix for translation errors --- src/main/java/emu/grasscutter/Grasscutter.java | 2 +- .../emu/grasscutter/server/dispatch/DispatchServer.java | 2 +- src/main/java/emu/grasscutter/utils/Language.java | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index d05939c05..8295cc599 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -93,7 +93,7 @@ public final class Grasscutter { ResourceLoader.loadAll(); ScriptLoader.init(); - // Database + // Initialize database. DatabaseManager.initialize(); // Create server instances. diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 1695e0208..7e4b1655e 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -334,7 +334,7 @@ public final class DispatchServer { return; } LoginResultJson responseData = new LoginResultJson(); - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt")); + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", req.ip())); // Login Account account = DatabaseHelper.getAccountById(requestData.uid); diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index ec56b41d9..8acb05cd4 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -29,7 +29,13 @@ public final class Language { * @return A translated value with arguments substituted. */ public static String translate(String key, Object... args) { - return Grasscutter.getLanguage().get(key).formatted(args); + String translated = Grasscutter.getLanguage().get(key); + try { + return translated.formatted(args); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to format string: " + key, exception); + return translated; + } } /** From bd7fb76b1dce6b0ef9f1cbcc4ce35196d3f72f46 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 19:12:54 -0700 Subject: [PATCH 154/434] Fix crash on login if the player didnt have a TowerManager --- src/main/java/emu/grasscutter/game/player/Player.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index ce505caa4..1c14ac09a 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -144,6 +144,7 @@ public class Player { this.avatars = new AvatarStorage(this); this.friendsList = new FriendsList(this); this.mailHandler = new MailHandler(this); + this.towerManager = new TowerManager(this); this.pos = new Position(); this.rotation = new Position(); this.properties = new HashMap<>(); @@ -190,7 +191,6 @@ public class Player { this.nickname = "Traveler"; this.signature = ""; this.teamManager = new TeamManager(this); - this.towerManager = new TowerManager(this); this.birthday = new PlayerBirthday(); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); @@ -1099,9 +1099,6 @@ public class Player { if (this.getProfile().getUid() == 0) { this.getProfile().syncWithCharacter(this); } - if (this.getTowerManager() == null) { - this.towerManager = new TowerManager(this); - } // Check if player object exists in server // TODO - optimize From 7b77f3eb9ea2fa1b23b53235600a58bd09d4c481 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 19:14:01 -0700 Subject: [PATCH 155/434] Add to gacha mappings to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 05ac741b2..35ee889c5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,5 @@ mongod.exe /*.sh language/ languages/ -gacha-mapping.js \ No newline at end of file +gacha-mapping.js +data/gacha_mappings.js From 4cf4d37738ab929dcd17f6d1ff26b2172b6217b9 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 19:16:36 -0700 Subject: [PATCH 156/434] Move server profile away from GameConstants --- src/main/java/emu/grasscutter/GameConstants.java | 6 ------ .../packet/send/PacketGetPlayerFriendListRsp.java | 13 +++++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/GameConstants.java b/src/main/java/emu/grasscutter/GameConstants.java index 4bebf9c81..dc07c32e1 100644 --- a/src/main/java/emu/grasscutter/GameConstants.java +++ b/src/main/java/emu/grasscutter/GameConstants.java @@ -11,13 +11,7 @@ public final class GameConstants { public static final int MAX_TEAMS = 4; public static final int MAIN_CHARACTER_MALE = 10000005; public static final int MAIN_CHARACTER_FEMALE = 10000007; - public static final String SERVER_AVATAR_NAME = Grasscutter.getConfig().getGameServerOptions().ServerNickname; - public static final int SERVER_AVATAR_ID = Grasscutter.getConfig().getGameServerOptions().ServerAvatarId; - public static final String SERVER_SIGNATURE = Grasscutter.getConfig().getGameServerOptions().ServerSignature; public static final Position START_POSITION = new Position(2747, 194, -1719); - public static final int SERVER_NAMECARD_ID = Grasscutter.getConfig().getGameServerOptions().ServerNameCardId; - public static final int SERVER_LEVEL = Grasscutter.getConfig().getGameServerOptions().ServerLevel; - public static final int SERVER_WORLD_LEVEL = Grasscutter.getConfig().getGameServerOptions().ServerWorldLevel; public static final int MAX_FRIENDS = 45; public static final int MAX_FRIEND_REQUESTS = 50; 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 781faf411..ff373140b 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java @@ -1,6 +1,7 @@ package emu.grasscutter.server.packet.send; import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; import emu.grasscutter.game.friends.Friendship; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; @@ -18,13 +19,13 @@ public class PacketGetPlayerFriendListRsp extends BasePacket { FriendBrief serverFriend = FriendBrief.newBuilder() .setUid(GameConstants.SERVER_CONSOLE_UID) - .setNickname(GameConstants.SERVER_AVATAR_NAME) - .setLevel(GameConstants.SERVER_LEVEL) - .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(GameConstants.SERVER_AVATAR_ID)) - .setWorldLevel(GameConstants.SERVER_WORLD_LEVEL) - .setSignature(GameConstants.SERVER_SIGNATURE) + .setNickname(Grasscutter.getConfig().getGameServerOptions().ServerNickname) + .setLevel(Grasscutter.getConfig().getGameServerOptions().ServerLevel) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(Grasscutter.getConfig().getGameServerOptions().ServerAvatarId)) + .setWorldLevel(Grasscutter.getConfig().getGameServerOptions().ServerWorldLevel) + .setSignature(Grasscutter.getConfig().getGameServerOptions().ServerSignature) .setLastActiveTime((int) (System.currentTimeMillis() / 1000f)) - .setNameCardId(GameConstants.SERVER_NAMECARD_ID) + .setNameCardId(Grasscutter.getConfig().getGameServerOptions().ServerNameCardId) .setOnlineState(FriendOnlineState.FRIEND_ONLINE) .setParam(1) .setIsGameSource(true) From 84f8023396ce32ae99ff58aa648ea10790084893 Mon Sep 17 00:00:00 2001 From: ShigemoriHakura <62388797+ShigemoriHakura@users.noreply.github.com> Date: Sat, 7 May 2022 10:50:13 +0800 Subject: [PATCH 157/434] Add all expedition rewards (#607) * fix achievement proto TakeAchievementRewardReq tells client the achievements achieved or not and it's progress. * add import * Add all AvatarExpedition protos Expedition system has almost done but still has some bug so it will be uploaded later * Add all expedition rewards --- data/ExpeditionReward.json | 960 +++++++++++++++++++++++++++++++++++++ 1 file changed, 960 insertions(+) diff --git a/data/ExpeditionReward.json b/data/ExpeditionReward.json index 90d9378b6..61c720f07 100644 --- a/data/ExpeditionReward.json +++ b/data/ExpeditionReward.json @@ -1,4 +1,724 @@ [ + { + "expId": 101, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101001, + "minCount": 4, + "maxCount": 5 + }, { + "itemId": 101002, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 1, + "maxCount": 2 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 3, + "maxCount": 4 + }, { + "itemId": 101003, + "minCount": 7, + "maxCount": 8 + } + ] + } + ] + }, + { + "expId": 102, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100064, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100064, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100064, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100064, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + } + { + "expId": 103, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101001, + "minCount": 4, + "maxCount": 5 + }, { + "itemId": 101002, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 1, + "maxCount": 2 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 3, + "maxCount": 4 + }, { + "itemId": 101003, + "minCount": 7, + "maxCount": 8 + } + ] + } + ] + }, + { + "expId": 104, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100062, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100062, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100062, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100062, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 105, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100013, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100013, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100013, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100013, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 106, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 625, + "maxCount": 625 + } + ] + }, + { + "hourTime": 8, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 1575, + "maxCount": 1575 + } + ] + }, + { + "hourTime": 12, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 2500, + "maxCount": 2500 + } + ] + }, + { + "hourTime": 20, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 5000, + "maxCount": 5000 + } + ] + } + ] + }, + { + "expId": 201, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 625, + "maxCount": 625 + } + ] + }, + { + "hourTime": 8, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 1575, + "maxCount": 1575 + } + ] + }, + { + "hourTime": 12, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 2500, + "maxCount": 2500 + } + ] + }, + { + "hourTime": 20, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 5000, + "maxCount": 5000 + } + ] + } + ] + }, + { + "expId": 202, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100063, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100063, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100063, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100063, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 203, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101001, + "minCount": 4, + "maxCount": 5 + }, { + "itemId": 101002, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 1, + "maxCount": 2 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 6, + "maxCount": 7 + }, { + "itemId": 101003, + "minCount": 2, + "maxCount": 3 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 101002, + "minCount": 3, + "maxCount": 4 + }, { + "itemId": 101003, + "minCount": 7, + "maxCount": 8 + } + ] + } + ] + }, + { + "expId": 204, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100013, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100013, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100013, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100013, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 205, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 625, + "maxCount": 625 + } + ] + }, + { + "hourTime": 8, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 1575, + "maxCount": 1575 + } + ] + }, + { + "hourTime": 12, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 2500, + "maxCount": 2500 + } + ] + }, + { + "hourTime": 20, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 5000, + "maxCount": 5000 + } + ] + } + ] + }, + { + "expId": 206, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100063, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100063, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100063, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100026, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100063, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, { "expId": 301, "expeditionRewardDataList": [ @@ -44,6 +764,181 @@ } ] }, + { + "expId": 302, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 101211, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 101211, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 101211, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100014, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 101211, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 303, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100002, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100002, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100002, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100012, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100002, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] + }, + { + "expId": 304, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 625, + "maxCount": 625 + } + ] + }, + { + "hourTime": 8, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 1575, + "maxCount": 1575 + } + ] + }, + { + "hourTime": 12, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 2500, + "maxCount": 2500 + } + ] + }, + { + "hourTime": 20, + "expeditionRewardData": [ + { + "itemId": 202, + "minCount": 5000, + "maxCount": 5000 + } + ] + } + ] + }, { "expId": 305, "expeditionRewardDataList": [ @@ -108,5 +1003,70 @@ ] } ] + }, + { + "expId": 306, + "expeditionRewardDataList": [ + { + "hourTime": 4, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 1, + "maxCount": 1 + }, { + "itemId": 100062, + "minCount": 1, + "maxCount": 1 + } + ] + }, + { + "hourTime": 8, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 2, + "maxCount": 4 + }, { + "itemId": 100062, + "minCount": 2, + "maxCount": 4 + } + ] + }, + { + "hourTime": 12, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 4, + "maxCount": 6 + }, { + "itemId": 100062, + "minCount": 4, + "maxCount": 6 + } + ] + }, + { + "hourTime": 20, + "rewardMora": 0, + "expeditionRewardData": [ + { + "itemId": 100061, + "minCount": 8, + "maxCount": 12 + }, { + "itemId": 100062, + "minCount": 8, + "maxCount": 12 + } + ] + } + ] } ] \ No newline at end of file From 5a33720e4f92c7f55f1b5e60f0026b6c2a0bec97 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 6 May 2022 20:04:25 -0700 Subject: [PATCH 158/434] Fix missing comma in expedition reward's json --- data/ExpeditionReward.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/ExpeditionReward.json b/data/ExpeditionReward.json index 61c720f07..b7c5db45d 100644 --- a/data/ExpeditionReward.json +++ b/data/ExpeditionReward.json @@ -128,7 +128,7 @@ ] } ] - } + }, { "expId": 103, "expeditionRewardDataList": [ From cc104a6a1f0adfc1759e6a0a4e75527325588a19 Mon Sep 17 00:00:00 2001 From: lhhxxxxx <91231470+lhhxxxxx@users.noreply.github.com> Date: Sat, 7 May 2022 12:20:44 +0800 Subject: [PATCH 159/434] Handle with undetermined locale language config (#611) * Update GiveAllCommand.java giveall command nomore give arts * Update Grasscutter.java * Update Grasscutter.java --- src/main/java/emu/grasscutter/Grasscutter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 823633c4f..89c4d8a26 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -149,7 +149,13 @@ public final class Grasscutter { public static void loadLanguage() { var locale = config.LocaleLanguage; - language = Language.getLanguage(locale.toLanguageTag()); + String languageTag = locale.toLanguageTag(); + if (languageTag.equals("und")) { + Grasscutter.getLogger().error("Illegal locale language, using en-US instead."); + language = Language.getLanguage("en-US"); + } else { + language = Language.getLanguage(languageTag); + } } public static void saveConfig() { From 0347d877ea41701a54645f2363e4df607ad518c1 Mon Sep 17 00:00:00 2001 From: FpguDhk <3411015214@qq.com> Date: Sat, 7 May 2022 11:38:14 +0800 Subject: [PATCH 160/434] Fix the Chinese messy code problem. --- src/main/java/emu/grasscutter/utils/Utils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index a72815f58..259fc8ad5 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -254,7 +254,7 @@ public final class Utils { */ public static String readFromInputStream(InputStream stream) { StringBuilder stringBuilder = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream,"UTF-8"))) { String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } stream.close(); From 9a611a4c06d24668323710082cab979cebb105ca Mon Sep 17 00:00:00 2001 From: Secretboy <74841174+Secretboy-SMR@users.noreply.github.com> Date: Sat, 7 May 2022 12:48:53 +0800 Subject: [PATCH 161/434] Update Config.java --- src/main/java/emu/grasscutter/Config.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 93e1480d2..1111ba920 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -23,6 +23,7 @@ public final class Config { public GameServerOptions GameServer = new GameServerOptions(); public DispatchServerOptions DispatchServer = new DispatchServerOptions(); public Locale LocaleLanguage = Locale.getDefault(); + public Locale DefaultLanguage = Locale.US; public Boolean OpenStamina = true; public GameServerOptions getGameServerOptions() { From 22ebf35e6993c7d68c007b8cdc09aa4496c141e5 Mon Sep 17 00:00:00 2001 From: Secretboy <74841174+Secretboy-SMR@users.noreply.github.com> Date: Sat, 7 May 2022 12:50:10 +0800 Subject: [PATCH 162/434] Update Language.java --- .../java/emu/grasscutter/utils/Language.java | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 8acb05cd4..340d35dc2 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -39,18 +39,47 @@ public final class Language { } /** - * Reads a file and creates a language instance. + * creates a language instance. * @param fileName The name of the language file. */ private Language(String fileName) { @Nullable JsonObject languageData = null; + + languageData = loadLanguage(fileName); + + if (languageData == null) { + Grasscutter.getLogger().info("Now switch to default language"); + languageData = loadDefaultLanguage(); + } + + assert languageData != null : "languageData is null"; + this.languageData = languageData; + } + + /** + * Load default language file and creates a language instance. + * @return language data + */ + private JsonObject loadDefaultLanguage() { + var fileName = Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json"; + return loadLanguage(fileName); + } + + /** + * Reads a file and creates a language instance. + * @param fileName The name of the language file. + * @return language data + */ + private JsonObject loadLanguage(String fileName) { + @Nullable JsonObject languageData = null; try { InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to load language file: " + fileName, exception); - } this.languageData = languageData; + Grasscutter.getLogger().warn("Failed to load language file: " + fileName); + } + return languageData; } /** From 9109e4ee44efcad8fd47f548836a01e58a9e6b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9C=9F=E5=BF=83?= <ceo@iqianye.cn> Date: Sat, 7 May 2022 13:45:51 +0800 Subject: [PATCH 163/434] Create zh-CN.json --- src/main/resources/languages/zh-CN.json | 298 ++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/main/resources/languages/zh-CN.json diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json new file mode 100644 index 000000000..acaf74bf4 --- /dev/null +++ b/src/main/resources/languages/zh-CN.json @@ -0,0 +1,298 @@ +{ + "messages": { + "game": { + "port_bind": "游戏服务器已在端口 %s 上启动", + "connect": "客户端已连接至 %s", + "disconnect": "客户端 %s 已断开连接", + "game_update_error": "游戏更新时发生错误", + "command_error": "命令发生错误:" + }, + "dispatch": { + "port_bind": "[Dispatch] 服务器已在端口 %s 上启动", + "request": "[Dispatch] 客户端 %s 请求: %s %s", + "keystore": { + "general_error": "[Dispatch] 加载 keystore 文件时发生错误!", + "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用预设的 keystore 密码...", + "no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 服务器", + "default_password": "[Dispatch] 默认的 keystore 密码加载成功。请考虑将 config.json 的默认密码设置为 123456" + }, + "no_commands_error": "此命令不适用于 Dispatch-only 模式", + "unhandled_request_error": "[Dispatch] 潜在的未处理请求 %s 请求:%s", + "account": { + "login_attempt": "[Dispatch] 客户端 %s 正在尝试登录", + "login_success": "[Dispatch] 客户端 %s 已登录,UID为 %s", + "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用令牌登录", + "login_token_error": "[Dispatch] 客户端 %s 使用令牌登录失败", + "login_token_success": "[Dispatch] 客户端 %s 已通过令牌登录,UID为 %s", + "combo_token_success": "[Dispatch] 客户端 %s 交换令牌成功", + "combo_token_error": "[Dispatch] 客户端 %s 交换令牌失败", + "account_login_create_success": "[Dispatch] 客户端 %s 登录失败: 已注册UID为 %s 的账号", + "account_login_create_error": "[Dispatch] 客户端 %s 登录失败:账号创建失败。", + "account_login_exist_error": "[Dispatch] 客户端 %s 登录失败:账号不存在", + "account_cache_error": "游戏账号缓存信息错误", + "session_key_error": "交换秘钥不符。", + "username_error": "未找到此用户名。", + "username_create_error": "未找到用户名,建立连接失败。" + } + }, + "status": { + "free_software": "Grasscutter 是免费开源软件。如果你是付费购买的,那已经被骗了。Github:https://github.com/Grasscutters/Grasscutter", + "starting": "正在启动 Grasscutter...", + "shutdown": "正在关闭...", + "done": "加載完成!输入 \"help\" 查看命令列表", + "error": "发生了一个错误。", + "welcome": "欢迎使用 Grasscutter", + "run_mode_error": "无效的服务器运行模式: %s。", + "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", + "create_resources": "正在创建 resources 目录...", + "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。" + } + }, + "commands": { + "generic": { + "not_specified": "没有指定命令。", + "unknown_command": "未知的命令:%s", + "permission_error": "您没有执行此命令的权限。", + "console_execute_error": "此命令只能在服务器控制台执行。", + "player_execute_error": "此命令只能在游戏内执行。", + "command_exist_error": "找不到命令。", + "invalid": { + "amount": "无效的 数量.", + "artifactId": "无效的圣遗物ID。", + "avatarId": "无效的角色ID。", + "avatarLevel": "无效的角色等級。", + "entityId": "无效的实体ID。", + "itemId": "无效的物品ID。", + "itemLevel": "无效的物品等級。", + "itemRefinement": "无效的物品精炼等级。", + "playerId": "无效的玩家ID。", + "uid": "无效的UID。" + } + }, + "execution": { + "uid_error": "无效的UID。", + "player_exist_error": "用户不存在。", + "player_offline_error": "玩家已离线。", + "item_id_error": "无效的物品ID。.", + "item_player_exist_error": "无效的物品/玩家UID。", + "entity_id_error": "无效的实体ID。", + "player_exist_offline_error": "玩家不存在或已离线。", + "argument_error": "无效的参数。", + "clear_target": "目标已清除.", + "set_target": "随后的的命令都会以@%s为预设。", + "need_target": "此命令需要一个目标 UID。添加 <@UID> 参数或使用 /target @UID 来设定持久目标。" + }, + "status": { + "enabled": "已启用", + "disabled": "未启用", + "help": "帮助", + "success": "成功" + }, + "account": { + "modify": "修改使用者账号", + "invalid": "无效的UID。", + "exists": "账号已存在。", + "create": "已建立账号,UID 为 %s 。", + "delete": "账号已刪除。", + "no_account": "账号不存在。", + "command_usage": "用法:account <create|delete> <username> [uid]" + }, + "broadcast": { + "command_usage": "用法:broadcast <消息>", + "message_sent": "公告已发送。" + }, + "changescene": { + "usage": "用法:changescene <scene id>", + "already_in_scene": "你已经在这个秘境中了。", + "success": "已切换至秘境 %s.", + "exists_error": "此秘境不存在。" + }, + "clear": { + "command_usage": "用法: clear <all|wp|art|mat>", + "weapons": "已将 %s 的武器清空。", + "artifacts": "已将 %s 的圣遗物清空。", + "materials": "已将 %s 的材料清空。", + "furniture": "已将 %s 的尘歌壶家具清空。", + "displays": "已清除 %s 的显示。", + "virtuals": "已将 %s 的所有货币和经验值清空。", + "everything": "已将 %s 的所有物品清空。" + }, + "coop": { + "usage": "用法:coop <playerId> <target playerId>", + "success": "已召唤 %s 到 %s的世界" + }, + "enter_dungeon": { + "usage": "用法:enterdungeon <dungeon id>", + "changed": "已进入秘境 %s", + "not_found_error": "此秘境不存在。", + "in_dungeon_error": "你已经在秘境中了。" + }, + "giveAll": { + "usage": "用法:giveall [player] [amount]", + "started": "正在给予全部物品...", + "success": "已给予全部物品。", + "invalid_amount_or_playerId": "无效的数量/玩家ID。" + }, + "giveArtifact": { + "usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "id_error": "无效的圣遗物ID。", + "success": "已将 %s 给予 %s。" + }, + "giveChar": { + "usage": "用法:givechar <player> <itemId|itemName> [amount]", + "given": "Given %s with level %s to %s.", + "invalid_avatar_id": "无效的角色ID。", + "invalid_avatar_level": "无效的角色等級。.", + "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。" + }, + "give": { + "usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]", + "refinement_only_applicable_weapons": "精炼等级参数仅在武器上可用", + "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", + "given": "已将 %s 个 %s 给予 %s。", + "given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s", + "given_level": "已将 %s 等级 %s %s 个给予 %s" + }, + "godmode": { + "success": "上帝模式被设置为 %s 。 [用户:%s]" + }, + "heal": { + "success": "所有角色已被治疗。" + }, + "kick": { + "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", + "server_kick_player": "正在踢出玩家 [%s:%s]" + }, + "kill": { + "usage": "用法:killall [playerUid] [sceneId]", + "scene_not_found_in_player_world": "未在玩家世界中找到此场景", + "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]" + }, + "killCharacter": { + "usage": "用法:/killcharacter [playerId]", + "success": "已杀死 %s 目前使用的角色。" + }, + "list": { + "message": "目前在线人数:%s" + }, + "permission": { + "usage": "用法:permission <add|remove> <username> <permission>", + "add": "已设置权限。", + "has_error": "此玩家已拥有此权限!", + "remove": "权限已移除。", + "not_have_error": "此玩家未拥有权限!", + "account_error": "账号不存在!" + }, + "position": { + "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d" + }, + "reload": { + "reload_start": "正在重载配置文件和数据。", + "reload_done": "重装完毕。" + }, + "resetConst": { + "reset_all": "重置所有角色的命座。", + "success": "已重置 %s 的命座,重新登录后将会生效。" + }, + "resetShopLimit": { + "usage": "用法:/resetshop <player id>" + }, + "sendMail": { + "usage": "用法:give [player] <itemId|itemName> [amount]", + "user_not_exist": "ID '%s' 的使用者不存在。", + "start_composition": "发送邮件流程。\n请使用`/send <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。", + "templates": "邮件模板尚未实装...", + "invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`", + "send_cancel": "取消发送邮件", + "send_done": "已将邮件给 %s!", + "send_all_done": "邮件已发送给所有人!", + "not_composition_end": "现在邮件发送未到最后阶段。\n请使用 `/sendmail %s` 继续发送邮件,或使用 `/sendmail stop` 来停止发送邮件。", + "please_use": "请使用 `/sendmail %s`", + "set_title": "成功将邮件标题设置为 '%s'。\n使用 '/sendmail <content>' 来设置邮件内容。", + "set_contents": "成功将'%s'设置为邮件内容。\n使用 '/sendmail <发件人>' 来设置发件人。", + "set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 来添加附件。", + "send": "已添加 %s 个 %s (等級为 %s) 邮件附件。\n如果没有要继续添加的道具请使用 `/sendmail finish` 来完成邮件发送。", + "invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`", + "title": "<标题>", + "message": "<正文>", + "sender": "<发件人>", + "arguments": "<itemId|itemName|finish> [数量] [等级]", + "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。" + }, + "sendMessage": { + "usage": "用法:sendmessage <player> <message>", + "success": "消息已发送。" + }, + "setFetterLevel": { + "usage": "用法:setfetterlevel <level>", + "range_error": "好感度等级必须在 0 到 10 之间。", + "fetter_set_level": "好感度已设置为 %s 级", + "level_error": "无效的好感度等级。" + }, + "setStats": { + "usage_console": "用法:setstats|stats @<UID> <stat> <value>", + "usage_ingame": "用法:setstats|stats [@UID] <stat> <value>", + "help_message": "\n\可使用的数据类型:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(cont.) 元素伤害:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)(cont.) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", + "value_error": "无效的数据值。", + "uid_error": "无效的UID。", + "player_error": "玩家不存在或已离线。", + "set_self": "%s 已经设置为 %s。", + "set_for_uid": "%s 的使用者 %s 更改为 %s。", + "set_max_hp": "最大生命值更改为 %s。" + }, + "setWorldLevel": { + "usage": "用法:setworldlevel <level>", + "value_error": "世界等级必须设置在0-8之间。", + "success": "已将世界等级设为%s。", + "invalid_world_level": "无效的世界等级。" + }, + "spawn": { + "usage": "用法:spawn <entityId> [amount] [level(仅限怪物]", + "success": "已生成 %s 个 %s。" + }, + "stop": { + "success": "正在关闭服务器..." + }, + "talent": { + "usage_1": "设置天赋等级:/talent set <talentID> <value>", + "usage_2": "另一种设置天赋等级的命令使用方法:/talent <n or e or q> <value>", + "usage_3": "获取天赋ID指令用法:/talent getid", + "lower_16": "无效的天赋等级,天赋等级应低于16。", + "set_id": "将天赋等级设为 %s。", + "set_atk": "将普通攻击等级设为 %s。", + "set_e": "设定天赋E等级为 %s。", + "set_q": "设定天赋Q等级为 %s。", + "invalid_skill_id": "无效的技能ID。", + "set_this": "将天赋等级设为 %s。", + "invalid_level": "无效的天赋等级。", + "normal_attack_id": "普通攻击的 ID 为 %s。", + "e_skill_id": "E技能ID %s。", + "q_skill_id": "Q技能ID %s。" + }, + "teleportAll": { + "success": "已将全部玩家传送到你的位置", + "error": "命令仅限多人游戏使用。" + }, + "teleport": { + "usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]", + "usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]", + "specify_player_id": "你必须指定一个玩家ID。", + "invalid_position": "无效的位置。", + "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s" + }, + "weather": { + "usage": "用法:weather <weatherId> [climateId]", + "success": "已将当前天气设定为 %s,气候则为 %s。", + "invalid_id": "无效的ID。" + }, + "drop": { + "command_usage": "用法:drop <itemId|itemName> [amount]", + "success": "已將 %s x %s 丟在附近。" + }, + "help": { + "usage": "用法:", + "aliases": "別名:", + "available_commands": "可用指令:" + } + } +} From 39816f8eeb5d462583d98f0a5e3d8e904668cba1 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Fri, 6 May 2022 23:12:15 -0700 Subject: [PATCH 164/434] Flight stamina cost -20% when Amber or Venti in team - Reduced stamina not tested in MP. - Stop MovementManager ticker when player goes offline. --- .../MovementManager/MovementManager.java | 191 +++++++++++------- .../emu/grasscutter/game/player/Player.java | 5 +- 2 files changed, 123 insertions(+), 73 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index 18958f355..8b8485f26 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -24,7 +24,7 @@ public class MovementManager { public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); - private enum Consumption { + private enum ConsumptionType { None(0), // consume @@ -46,11 +46,22 @@ public class MovementManager { POWERED_FLY(500); public final int amount; - Consumption(int amount) { + ConsumptionType(int amount) { this.amount = amount; } } + private class Consumption { + public ConsumptionType consumptionType; + public int amount; + public Consumption(ConsumptionType ct, int a) { + consumptionType = ct; + amount = a; + } + public Consumption(ConsumptionType ct) { + this(ct, ct.amount); + } + } private MotionState previousState = MotionState.MOTION_STANDBY; private MotionState currentState = MotionState.MOTION_STANDBY; @@ -139,6 +150,7 @@ public class MovementManager { } public void resetTimer() { + Grasscutter.getLogger().debug("MovementManager ticker stopped"); movementManagerTickTimer.cancel(); movementManagerTickTimer = null; } @@ -269,95 +281,39 @@ public class MovementManager { boolean moving = isPlayerMoving(); if (moving || (getCurrentStamina() < getMaximumStamina())) { // Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); - Consumption consumption = Consumption.None; + Consumption consumption = new Consumption(ConsumptionType.None); // TODO: refactor these conditions. if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - if (currentState == MotionState.MOTION_CLIMB) { - // CLIMB - if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = Consumption.CLIMB_START; - } else { - consumption = Consumption.CLIMBING; - } - } - if (currentState == MotionState.MOTION_CLIMB_JUMP) { - if (previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = Consumption.CLIMB_JUMP; - } - } - if (currentState == MotionState.MOTION_JUMP) { - if (previousState == MotionState.MOTION_CLIMB) { - consumption = Consumption.CLIMB_JUMP; - } - } + consumption = getClimbConsumption(); } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { - // SWIM - if (currentState == MotionState.MOTION_SWIM_MOVE) { - consumption = Consumption.SWIMMING; - } - if (currentState == MotionState.MOTION_SWIM_DASH) { - if (previousState != MotionState.MOTION_SWIM_DASH) { - consumption = Consumption.SWIM_DASH_START; - } else { - consumption = Consumption.SWIM_DASH; - } - } + consumption = getSwimConsumptions(); } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - // RUN, DASH and WALK - // DASH - if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) { - consumption = Consumption.DASH; - if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) { - // only charge once - consumption = Consumption.SPRINT; - } - } - if (currentState == MotionState.MOTION_DASH) { - consumption = Consumption.SPRINT; - } - // RUN - if (currentState == MotionState.MOTION_RUN) { - consumption = Consumption.RUN; - } - // WALK - if (currentState == MotionState.MOTION_WALK) { - consumption = Consumption.WALK; - } + consumption = getRunWalkDashConsumption(); } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - // FLY - consumption = Consumption.FLY; - // POWERED_FLY, e.g. wind tunnel - if (currentState == MotionState.MOTION_POWERED_FLY) { - consumption = Consumption.POWERED_FLY; - } + consumption = getFlyConsumption(); } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - // STAND - if (currentState == MotionState.MOTION_STANDBY) { - consumption = Consumption.STANDBY; - } - if (currentState == MotionState.MOTION_STANDBY_MOVE) { - consumption = Consumption.STANDBY_MOVE; - } + consumption = getStandConsumption(); } - // tick triggered - handleDrowning(); - + // delay 2 seconds before start recovering - as official server does. if (cachedSession != null) { if (consumption.amount < 0) { staminaRecoverDelay = 0; } - if (consumption.amount > 0) { + if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { if (staminaRecoverDelay < 10) { staminaRecoverDelay++; - consumption = Consumption.None; + consumption = new Consumption(ConsumptionType.None); } } - int newStamina = updateStamina(cachedSession, consumption.amount); + Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); + updateStamina(cachedSession, consumption.amount); cachedSession.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t" + consumption + "(" + consumption.amount + ")"); } + + // tick triggered + handleDrowning(); } } @@ -366,4 +322,95 @@ public class MovementManager { currentCoordinates.getY(), currentCoordinates.getZ());; } } + + private Consumption getClimbConsumption() { + Consumption consumption = new Consumption(ConsumptionType.None); + if (currentState == MotionState.MOTION_CLIMB) { + consumption = new Consumption(ConsumptionType.CLIMBING); + if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = new Consumption(ConsumptionType.CLIMB_START); + } + if (!isPlayerMoving()) { + consumption = new Consumption(ConsumptionType.None); + } + } + if (currentState == MotionState.MOTION_CLIMB_JUMP) { + if (previousState != MotionState.MOTION_CLIMB_JUMP) { + consumption = new Consumption(ConsumptionType.CLIMB_JUMP); + } + } + return consumption; + } + + // TODO: Kamisato Ayaka & Mona + + private Consumption getSwimConsumptions() { + Consumption consumption = new Consumption(ConsumptionType.None); + if (currentState == MotionState.MOTION_SWIM_MOVE) { + consumption = new Consumption(ConsumptionType.SWIMMING); + } + if (currentState == MotionState.MOTION_SWIM_DASH) { + consumption = new Consumption(ConsumptionType.SWIM_DASH_START); + if (previousState == MotionState.MOTION_SWIM_DASH) { + consumption = new Consumption(ConsumptionType.SWIM_DASH); + } + } + return consumption; + } + + private Consumption getRunWalkDashConsumption() { + Consumption consumption = new Consumption(ConsumptionType.None); + if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) { + consumption = new Consumption(ConsumptionType.DASH); + if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) { + // only charge once + consumption = new Consumption(ConsumptionType.SPRINT); + } + } + if (currentState == MotionState.MOTION_DASH) { + consumption = new Consumption(ConsumptionType.SPRINT); + } + if (currentState == MotionState.MOTION_RUN) { + consumption = new Consumption(ConsumptionType.RUN); + } + if (currentState == MotionState.MOTION_WALK) { + consumption = new Consumption(ConsumptionType.WALK); + } + return consumption; + } + + private Consumption getFlyConsumption() { + Consumption consumption = new Consumption(ConsumptionType.FLY); + HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ + put(212301, 0.8f); // Amber + put(222301, 0.8f); // Venti + }}; + float reduction = 1; + for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) { + for (int skillId: entity.getAvatar().getProudSkillList()) { + if (glidingCostReduction.containsKey(skillId)) { + reduction = glidingCostReduction.get(skillId); + } + } + } + consumption.amount *= reduction; + + // POWERED_FLY, e.g. wind tunnel + if (currentState == MotionState.MOTION_POWERED_FLY) { + consumption = new Consumption(ConsumptionType.POWERED_FLY); + } + return consumption; + } + + private Consumption getStandConsumption() { + Consumption consumption = new Consumption(ConsumptionType.None); + if (currentState == MotionState.MOTION_STANDBY) { + consumption = new Consumption(ConsumptionType.STANDBY); + } + if (currentState == MotionState.MOTION_STANDBY_MOVE) { + consumption = new Consumption(ConsumptionType.STANDBY_MOVE); + } + return consumption; + } } + diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 1c14ac09a..1eb5e3526 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -1151,8 +1151,11 @@ public class Player { } public void onLogout() { + // stop stamina calculation + getMovementManager().resetTimer(); + // force to leave the dungeon - if(getScene().getSceneType() == SceneType.SCENE_DUNGEON){ + if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) { this.getServer().getDungeonManager().exitDungeon(this); } // Leave world From f1f3badd16048de10eedb28f8afedc3f932a7025 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Fri, 6 May 2022 22:47:36 -0700 Subject: [PATCH 165/434] Fix gacha mapping generation login * Fix `System#out` usage pointed by magix at https://github.com/Grasscutters/Grasscutter/pull/568#pullrequestreview-965271278 * Fix typos for interchange `-` and `_`. * Fix gacha mapping autogeneration path --- .../java/emu/grasscutter/Grasscutter.java | 2 +- .../java/emu/grasscutter/tools/Tools.java | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 89c4d8a26..51bc3fcc5 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -77,7 +77,7 @@ public final class Grasscutter { Tools.createGmHandbook(); exitEarly = true; } case "-gachamap" -> { - Tools.createGachaMapping("./gacha-mapping.js"); exitEarly = true; + Tools.createGachaMapping(Grasscutter.getConfig().DATA_FOLDER + "/gacha_mappings.js"); exitEarly = true; } } } diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index a2bee91f0..7429c143f 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -64,25 +64,26 @@ public final class Tools { if (availableLangList.size() == 1) { return availableLangList.get(0).toUpperCase(); } - System.out.println("The following languages mappings are available, please select one: [default: EN]"); - String groupedLangList = "> "; + String stagedMessage = ""; + stagedMessage += "The following languages mappings are available, please select one: [default: EN]\n"; + String groupedLangList = ">\t"; int groupedLangCount = 0; String input = ""; for (String availableLanguage: availableLangList){ groupedLangCount++; groupedLangList = groupedLangList + "" + availableLanguage + "\t"; if (groupedLangCount == 6) { - System.out.println(groupedLangList); + stagedMessage += groupedLangList + "\n"; groupedLangCount = 0; - groupedLangList = "> "; + groupedLangList = ">\t"; } } if (groupedLangCount > 0) { - System.out.println(groupedLangList); + stagedMessage += groupedLangList + "\n"; } - System.out.print("\nYour choice:[EN] "); - - input = new BufferedReader(new InputStreamReader(System.in)).readLine(); + stagedMessage += "\nYour choice:[EN] "; + + input = Grasscutter.getConsole().readLine(stagedMessage); if (availableLangList.contains(input.toLowerCase())) { return input.toUpperCase(); } @@ -249,6 +250,6 @@ final class ToolsWithLanguageOption { writer.println("}\n}"); } - Grasscutter.getLogger().info("Mappings generated!"); + Grasscutter.getLogger().info("Mappings generated to " + location + " !"); } } From 29c5551450ecf862112899e410db7bba46c378e7 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 00:23:56 -0700 Subject: [PATCH 166/434] Prepare MovementManager.updateStamina() for external calls. --- .../game/managers/MovementManager/MovementManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index 8b8485f26..60de57055 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -179,8 +179,6 @@ public class MovementManager { return player.getProperty(PlayerProperty.PROP_MAX_STAMINA); } - - // Returns new stamina public int updateStamina(GameSession session, int amount) { int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); @@ -196,6 +194,7 @@ public class MovementManager { newStamina = playerMaxStamina; } session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); return newStamina; } @@ -309,7 +308,6 @@ public class MovementManager { } Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); updateStamina(cachedSession, consumption.amount); - cachedSession.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); } // tick triggered From 34af72ec9eeb3b5b02d26cbc823a8842f4bedd91 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 00:37:27 -0700 Subject: [PATCH 167/434] Kamisato Ayaka and Mona talent moving costs stamina --- .../MovementManager/MovementManager.java | 38 ++++++++++++++++--- .../recv/HandlerEvtDoSkillSuccNotify.java | 6 +-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index 60de57055..23b45903a 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -37,6 +37,7 @@ public class MovementManager { SWIM_DASH_START(-200), SWIM_DASH(-200), SWIMMING(-80), + FIGHT(0), // restore STANDBY(500), @@ -75,8 +76,9 @@ public class MovementManager { private Timer movementManagerTickTimer; private GameSession cachedSession = null; private GameEntity cachedEntity = null; - private int staminaRecoverDelay = 0; + private int skillCaster = 0; + private int skillCasting = 0; public MovementManager(Player player) { previousCoordinates.add(new Position(0,0,0)); @@ -125,6 +127,12 @@ public class MovementManager { MotionState.MOTION_WALK, MotionState.MOTION_DANGER_WALK ))); + + MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( + MotionState.MOTION_FIGHT + ))); + + } public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) { @@ -145,7 +153,7 @@ public class MovementManager { currentCoordinates = newPos; } currentState = motionInfo.getState(); - Grasscutter.getLogger().debug("" + currentState); + Grasscutter.getLogger().debug("" + currentState + "\t" + (moveInfo.getIsReliable() ? "reliable" : "")); handleFallOnGround(motionInfo); } @@ -293,6 +301,8 @@ public class MovementManager { consumption = getFlyConsumption(); } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { consumption = getStandConsumption(); + } else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) { + consumption = getFightConsumption(); } // delay 2 seconds before start recovering - as official server does. @@ -306,7 +316,7 @@ public class MovementManager { consumption = new Consumption(ConsumptionType.None); } } - Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); + // Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); updateStamina(cachedSession, consumption.amount); } @@ -340,8 +350,6 @@ public class MovementManager { return consumption; } - // TODO: Kamisato Ayaka & Mona - private Consumption getSwimConsumptions() { Consumption consumption = new Consumption(ConsumptionType.None); if (currentState == MotionState.MOTION_SWIM_MOVE) { @@ -410,5 +418,25 @@ public class MovementManager { } return consumption; } + + private Consumption getFightConsumption() { + Consumption consumption = new Consumption(ConsumptionType.None); + HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ + put(10013, -1000); // Kamisato Ayaka + put(10413, -1000); // Mona + }}; + if (fightingCost.containsKey(skillCasting)) { + consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting)); + // only handle once, so reset. + skillCasting = 0; + skillCaster = 0; + } + return consumption; + } + + public void notifySkill(int caster, int skillId) { + skillCaster = caster; + skillCasting = skillId; + } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java index d1db944d2..a57ae9665 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -16,11 +16,9 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { // TODO: Will be used for deducting stamina for charged skills. int caster = notify.getCasterId(); - int skill = notify.getSkillId(); + int skillId = notify.getSkillId(); - // Grasscutter.getLogger().warn(caster + "\t" + skill); - -// session.getPlayer().getScene().broadcastPacket(new PacketEvtAvatarStandUpNotify(notify)); + session.getPlayer().getMovementManager().notifySkill(caster, skillId); } } From d8477fbcc46cd755d3b17093a08f1f290abccf4c Mon Sep 17 00:00:00 2001 From: kaitl <54618947+kaitl@users.noreply.github.com> Date: Sat, 7 May 2022 15:28:25 +0800 Subject: [PATCH 168/434] Update zh-CN.json --- src/main/resources/languages/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index acaf74bf4..b9bc5b22a 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -232,7 +232,7 @@ "setStats": { "usage_console": "用法:setstats|stats @<UID> <stat> <value>", "usage_ingame": "用法:setstats|stats [@UID] <stat> <value>", - "help_message": "\n\可使用的数据类型:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(cont.) 元素伤害:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)(cont.) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", + "help_message": "\n\t可使用的数据类型:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(cont.) 元素伤害:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)(cont.) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", "value_error": "无效的数据值。", "uid_error": "无效的UID。", "player_error": "玩家不存在或已离线。", From 5cc9ecfd91ead78072da6eb1949a42b5631ada98 Mon Sep 17 00:00:00 2001 From: muhammadeko <muhammadekoprasetyo29@gmail.com> Date: Sat, 7 May 2022 19:48:20 +0700 Subject: [PATCH 169/434] PluginManager: Use the same class loader and add getPlugin method --- .../emu/grasscutter/plugin/PluginManager.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java index 7a5e0aa00..4844a698a 100644 --- a/src/main/java/emu/grasscutter/plugin/PluginManager.java +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -4,12 +4,12 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.server.event.Event; import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.HandlerPriority; -import emu.grasscutter.utils.EventConsumer; import emu.grasscutter.utils.Utils; import java.io.File; import java.io.InputStreamReader; import java.lang.reflect.Method; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.*; @@ -47,12 +47,23 @@ public final class PluginManager { List<File> plugins = Arrays.stream(files) .filter(file -> file.getName().endsWith(".jar")) .toList(); - + + URL[] pluginNames = new URL[plugins.size()]; + plugins.forEach(plugin -> { + try { + pluginNames[plugins.indexOf(plugin)] = plugin.toURI().toURL(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + }); + + URLClassLoader classLoader = new URLClassLoader(pluginNames); + plugins.forEach(plugin -> { try { URL url = plugin.toURI().toURL(); try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { - URL configFile = loader.findResource("plugin.json"); + URL configFile = loader.findResource("plugin.json"); // Find the plugin.json file for each plugin. InputStreamReader fileReader = new InputStreamReader(configFile.openStream()); PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); @@ -68,10 +79,10 @@ public final class PluginManager { JarEntry entry = entries.nextElement(); if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue; String className = entry.getName().replace(".class", "").replace("/", "."); - loader.loadClass(className); + classLoader.loadClass(className); //For all plugin we use the same class loader. } - Class<?> pluginClass = loader.loadClass(pluginConfig.mainClass); + Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader); @@ -156,6 +167,10 @@ public final class PluginManager { .toList().forEach(handler -> this.invokeHandler(event, handler)); } + public Plugin getPlugin(String name) { + return this.plugins.get(name); + } + /** * Performs logic checks then invokes the provided event handler. * @param event The event passed through to the handler. From d20cffb90557e5c1e548130f9959349bc3583bc8 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 21:47:13 +0800 Subject: [PATCH 170/434] Monsters tide turn by turn && Ban User Skill && Lua functions --- proto/CanUseSkillNotify.proto | 15 ++ .../game/entity/EntityMonster.java | 1 + .../grasscutter/game/tower/TowerManager.java | 4 +- .../emu/grasscutter/game/world/Scene.java | 8 +- .../scripts/SceneScriptManager.java | 151 +++++++++++------- .../emu/grasscutter/scripts/ScriptLib.java | 123 +++++++++++--- .../grasscutter/scripts/data/SceneGroup.java | 12 +- .../packet/send/PacketCanUseSkillNotify.java | 19 +++ src/main/resources/logback.xml | 2 + 9 files changed, 247 insertions(+), 88 deletions(-) create mode 100644 proto/CanUseSkillNotify.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java diff --git a/proto/CanUseSkillNotify.proto b/proto/CanUseSkillNotify.proto new file mode 100644 index 000000000..60ac6d7f0 --- /dev/null +++ b/proto/CanUseSkillNotify.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CanUseSkillNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1019; + } + + bool is_can_use_skill = 1; +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java index c9d0c0982..0ae6f356b 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java @@ -117,6 +117,7 @@ public class EntityMonster extends GameEntity { this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); } if (getScene().getScriptManager().isInit() && this.getGroupId() > 0) { + getScene().getScriptManager().onMonsterDie(); getScene().getScriptManager().callEvent(EventType.EVENT_ANY_MONSTER_DIE, null); } if (getScene().getChallenge() != null && getScene().getChallenge().getGroup().id == this.getGroupId()) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 51f840663..409549a1f 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -7,6 +7,7 @@ import emu.grasscutter.data.def.TowerLevelData; import emu.grasscutter.game.dungeons.DungeonSettleListener; import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; @@ -75,7 +76,8 @@ public class TowerManager { player.getScene().setPrevScenePoint(enterPointId); player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, currentLevel)); - + // stop using skill + player.getSession().send(new PacketCanUseSkillNotify(false)); } public void notifyCurLevelRecordChange(){ diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 97099c9b9..82ce9139f 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -105,7 +105,13 @@ public class Scene { public GameEntity getEntityById(int id) { return this.entities.get(id); } - + + public GameEntity getEntityByConfigId(int configId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId) + .findFirst() + .orElse(null); + } /** * @return the autoCloseTime */ diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 5f6a1b7e6..b8ba800a6 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -1,19 +1,14 @@ package emu.grasscutter.scripts; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptException; -import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; import org.luaj.vm2.lib.jse.CoerceJavaToLua; @@ -23,12 +18,8 @@ import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.WorldLevelData; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.constants.ScriptGadgetState; -import emu.grasscutter.scripts.constants.ScriptRegionShape; import emu.grasscutter.scripts.data.SceneBlock; import emu.grasscutter.scripts.data.SceneConfig; import emu.grasscutter.scripts.data.SceneGadget; @@ -56,7 +47,12 @@ public class SceneScriptManager { private final Int2ObjectOpenHashMap<Set<SceneTrigger>> triggers; private final Int2ObjectOpenHashMap<SceneRegion> regions; - + private SceneGroup currentGroup; + private AtomicInteger monsterAlive; + private AtomicInteger monsterTideCount; + private int monsterSceneLimit; + private ConcurrentLinkedQueue<Integer> monsterOrders; + public SceneScriptManager(Scene scene) { this.scene = scene; this.scriptLib = new ScriptLib(this); @@ -222,7 +218,8 @@ public class SceneScriptManager { cs.eval(getBindings()); // Set - group.monsters = ScriptLoader.getSerializer().toList(SceneMonster.class, bindings.get("monsters")); + group.monsters = ScriptLoader.getSerializer().toList(SceneMonster.class, bindings.get("monsters")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y)); group.gadgets = ScriptLoader.getSerializer().toList(SceneGadget.class, bindings.get("gadgets")); group.triggers = ScriptLoader.getSerializer().toList(SceneTrigger.class, bindings.get("triggers")); group.suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites")); @@ -235,7 +232,7 @@ public class SceneScriptManager { // Add monsters to suite TODO optimize Int2ObjectMap<Object> map = new Int2ObjectOpenHashMap<>(); - group.monsters.forEach(m -> map.put(m.config_id, m)); + group.monsters.entrySet().forEach(m -> map.put(m.getValue().config_id, m)); group.gadgets.forEach(m -> map.put(m.config_id, m)); for (SceneSuite suite : group.suites) { @@ -323,60 +320,92 @@ public class SceneScriptManager { } public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { - spawnMonstersInGroup(group, group.getSuiteByIndex(suiteIndex)); + this.currentGroup = group; + this.monsterSceneLimit = 0; + var suite = group.getSuiteByIndex(suiteIndex); + if(suite == null){ + return; + } + suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); } public void spawnMonstersInGroup(SceneGroup group) { - spawnMonstersInGroup(group, null); + this.currentGroup = group; + this.monsterSceneLimit = 0; + group.monsters.values().forEach(mob -> spawnMonstersInGroup(group, mob)); } - - public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { - List<SceneMonster> monsters = group.monsters; - - if (suite != null) { - monsters = suite.sceneMonsters; + public void spawnMonstersInGroup(SceneGroup group,Integer[] ordersConfigId, int tideCount, int sceneLimit) { + this.currentGroup = group; + this.monsterSceneLimit = sceneLimit; + this.monsterTideCount = new AtomicInteger(tideCount); + this.monsterAlive = new AtomicInteger(0); + this.monsterOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); + + // add the last turn + group.monsters.keySet().stream() + .filter(i -> !this.monsterOrders.contains(i)) + .forEach(this.monsterOrders::add); + for (int i = 0; i < sceneLimit; i++) { + spawnMonstersInGroup(group, group.monsters.get(this.monsterOrders.poll())); + } + } + public void spawnMonstersInGroup(SceneGroup group, SceneMonster monster) { + if(monster == null){ + return; + } + if(this.monsterSceneLimit > 0){ + this.monsterTideCount.decrementAndGet(); + this.monsterAlive.incrementAndGet(); } - List<GameEntity> toAdd = new ArrayList<>(); - - for (SceneMonster monster : monsters) { - MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); - - if (data == null) { - continue; - } - - // Calculate level - int level = monster.level; - - if (getScene().getDungeonData() != null) { - level = getScene().getDungeonData().getShowLevel(); - } else if (getScene().getWorld().getWorldLevel() > 0) { - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); - - if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); - } - } - - // Spawn mob - EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); - entity.getRotation().set(monster.rot); - entity.setGroupId(group.id); - entity.setConfigId(monster.config_id); - - toAdd.add(entity); + MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); + + if (data == null) { + return; } - - if (toAdd.size() > 0) { - getScene().addEntities(toAdd); - - for (GameEntity entity : toAdd) { - callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); + + // Calculate level + int level = monster.level; + + if (getScene().getDungeonData() != null) { + level = getScene().getDungeonData().getShowLevel(); + } else if (getScene().getWorld().getWorldLevel() > 0) { + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); + + if (worldLevelData != null) { + level = worldLevelData.getMonsterLevel(); + } + } + + // Spawn mob + EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); + entity.getRotation().set(monster.rot); + entity.setGroupId(group.id); + entity.setConfigId(monster.config_id); + + getScene().addEntity(entity); + + callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); + } + + public void onMonsterDie(){ + if(this.monsterSceneLimit <= 0){ + return; + } + if(this.monsterAlive.decrementAndGet() >= this.monsterSceneLimit) { + // maybe not happen + return; + } + if(this.monsterTideCount.get() > 0){ + // add more + spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); + }else if(this.monsterAlive.get() == 0){ + // spawn the last turn of monsters + while(!this.monsterOrders.isEmpty()){ + spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); } } } - // Events public void callEvent(int eventType, ScriptArgs params) { @@ -405,4 +434,8 @@ public class SceneScriptManager { } } } + +// public LuaValue safetyCall(){ +// +// } } diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 941b00b60..1b9badc11 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -1,28 +1,24 @@ package emu.grasscutter.scripts; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.game.dungeons.DungeonChallenge; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityMonster; import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.SceneRegion; -import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketGadgetStateNotify; import emu.grasscutter.server.packet.send.PacketWorktopOptionNotify; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; public class ScriptLib { + public static final Logger logger = LoggerFactory.getLogger(ScriptLib.class); private final SceneScriptManager sceneScriptManager; public ScriptLib(SceneScriptManager sceneScriptManager) { @@ -34,6 +30,8 @@ public class ScriptLib { } public int SetGadgetStateByConfigId(int configId, int gadgetState) { + logger.debug("[LUA] Call SetGadgetStateByConfigId with {},{}", + configId,gadgetState); Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId).findFirst(); @@ -53,6 +51,8 @@ public class ScriptLib { } public int SetGroupGadgetStateByConfigId(int groupId, int configId, int gadgetState) { + logger.debug("[LUA] Call SetGroupGadgetStateByConfigId with {},{},{}", + groupId,configId,gadgetState); List<GameEntity> list = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getGroupId() == groupId).toList(); @@ -71,6 +71,8 @@ public class ScriptLib { } public int SetWorktopOptionsByGroupId(int groupId, int configId, int[] options) { + logger.debug("[LUA] Call SetWorktopOptionsByGroupId with {},{},{}", + groupId,configId,options); Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId).findFirst(); @@ -90,6 +92,8 @@ public class ScriptLib { } public int DelWorktopOptionByGroupId(int groupId, int configId, int option) { + logger.debug("[LUA] Call DelWorktopOptionByGroupId with {},{},{}",groupId,configId,option); + Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId).findFirst(); @@ -109,20 +113,24 @@ public class ScriptLib { } // Some fields are guessed - public int AutoMonsterTide(int challengeIndex, int groupId, int[] config_ids, int param4, int param5, int param6) { + public int AutoMonsterTide(int challengeIndex, int groupId, Integer[] ordersConfigId, int tideCount, int sceneLimit, int param6) { + logger.debug("[LUA] Call AutoMonsterTide with {},{},{},{},{},{}", + challengeIndex,groupId,ordersConfigId,tideCount,sceneLimit,param6); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); if (group == null || group.monsters == null) { return 1; } - - // TODO just spawn all from group for now - this.getSceneScriptManager().spawnMonstersInGroup(group); + + this.getSceneScriptManager().spawnMonstersInGroup(group, ordersConfigId, tideCount, sceneLimit); return 0; } public int AddExtraGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call AddExtraGroupSuite with {},{}", + groupId,suite); SceneGroup group = getSceneScriptManager().getGroupById(groupId); if (group == null || group.monsters == null) { @@ -136,8 +144,17 @@ public class ScriptLib { } // param3 (probably time limit for timed dungeons) - public int ActiveChallenge(int challengeId, int challengeIndex, int param3, int groupId, int objectiveKills, int param5) { + public int ActiveChallenge(int challengeId, int challengeIndex, int timeLimitOrGroupId, int groupId, int objectiveKills, int param5) { + logger.debug("[LUA] Call ActiveChallenge with {},{},{},{},{},{}", + challengeId,challengeIndex,timeLimitOrGroupId,groupId,objectiveKills,param5); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + var objective = objectiveKills; + + if(group == null){ + group = getSceneScriptManager().getGroupById(timeLimitOrGroupId); + objective = groupId; + } if (group == null || group.monsters == null) { return 1; @@ -146,7 +163,7 @@ public class ScriptLib { DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), group); challenge.setChallengeId(challengeId); challenge.setChallengeIndex(challengeIndex); - challenge.setObjective(objectiveKills); + challenge.setObjective(objective); getSceneScriptManager().getScene().setChallenge(challenge); @@ -155,26 +172,37 @@ public class ScriptLib { } public int GetGroupMonsterCountByGroupId(int groupId) { + logger.debug("[LUA] Call GetGroupMonsterCountByGroupId with {}", + groupId); return (int) getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e instanceof EntityMonster && e.getGroupId() == groupId) .count(); } public int GetGroupVariableValue(String var) { + logger.debug("[LUA] Call GetGroupVariableValue with {}", + var); return getSceneScriptManager().getVariables().getOrDefault(var, 0); } public int SetGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call SetGroupVariableValue with {},{}", + var, value); getSceneScriptManager().getVariables().put(var, value); return 0; } public LuaValue ChangeGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call ChangeGroupVariableValue with {},{}", + var, value); + getSceneScriptManager().getVariables().put(var, getSceneScriptManager().getVariables().get(var) + value); return LuaValue.ZERO; } public int RefreshGroup(LuaTable table) { + logger.debug("[LUA] Call RefreshGroup with {}", + table); // Kill and Respawn? int groupId = table.get("group_id").toint(); int suite = table.get("suite").toint(); @@ -192,6 +220,8 @@ public class ScriptLib { } public int GetRegionEntityCount(LuaTable table) { + logger.debug("[LUA] Call GetRegionEntityCount with {}", + table); int regionId = table.get("region_eid").toint(); int entityType = table.get("entity_type").toint(); @@ -205,21 +235,68 @@ public class ScriptLib { } public void PrintContextLog(String msg) { - Grasscutter.getLogger().info("[LUA] " + msg); + logger.info("[LUA] " + msg); } - public int TowerCountTimeStatus(int var1, int var2){ + public int TowerCountTimeStatus(int isDone, int var2){ + logger.debug("[LUA] Call TowerCountTimeStatus with {},{}", + isDone,var2); + // TODO record time return 0; } public int GetGroupMonsterCount(int var1){ - // Maybe... - return GetGroupMonsterCountByGroupId(var1); + logger.debug("[LUA] Call GetGroupMonsterCount with {}", + var1); + + return (int) getSceneScriptManager().getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster) + .count(); } public int SetMonsterBattleByGroup(int var1, int var2, int var3){ + logger.debug("[LUA] Call SetMonsterBattleByGroup with {},{},{}", + var1,var2,var3); + return 0; } public int CauseDungeonFail(int var1){ + logger.debug("[LUA] Call CauseDungeonFail with {}", + var1); + return 0; } + // 8-1 + public int GetGroupVariableValueByGroup(int var1, String var2, int var3){ + logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{},{}", + var1,var2,var3); + + //TODO + + return getSceneScriptManager().getVariables().getOrDefault(var2, 0); + } + + public int SetIsAllowUseSkill(int canUse, int var2){ + logger.debug("[LUA] Call SetIsAllowUseSkill with {},{}", + canUse,var2); + + getSceneScriptManager().getScene().broadcastPacket(new PacketCanUseSkillNotify(canUse == 1)); + return 0; + } + + public int KillEntityByConfigId(LuaTable table){ + logger.debug("[LUA] Call KillEntityByConfigId with {}", + table); + var configId = table.get("config_id"); + if(configId == LuaValue.NIL){ + return 1; + } + + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId.toint()); + if(entity == null){ + return 1; + } + getSceneScriptManager().getScene().killEntity(entity, 0); + return 0; + } + } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index a13db7b68..690cd3d0d 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -1,17 +1,21 @@ package emu.grasscutter.scripts.data; -import java.util.List; - import emu.grasscutter.utils.Position; +import java.util.List; +import java.util.Map; + public class SceneGroup { public transient int block_id; // Not an actual variable in the scripts but we will keep it here for reference public int id; public int refresh_id; public Position pos; - - public List<SceneMonster> monsters; + + /** + * ConfigId - Monster + */ + public Map<Integer,SceneMonster> monsters; public List<SceneGadget> gadgets; public List<SceneTrigger> triggers; public List<SceneRegion> regions; diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java new file mode 100644 index 000000000..f8fe1314a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CanUseSkillNotifyOuterClass; + +public class PacketCanUseSkillNotify extends BasePacket { + + public PacketCanUseSkillNotify(boolean canUseSkill) { + super(PacketOpcodes.CanUseSkillNotify); + + CanUseSkillNotifyOuterClass.CanUseSkillNotify proto = CanUseSkillNotifyOuterClass.CanUseSkillNotify.newBuilder() + .setIsCanUseSkill(canUseSkill) + .build(); + + this.setData(proto); + } + +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 91d3f133c..1fc6831cb 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -19,4 +19,6 @@ <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> + <logger name="emu.grasscutter.scripts.ScriptLib" level="DEBUG"> + </logger> </Configuration> \ No newline at end of file From 8618c7de9e6a2e80eb405a2e72d9d6e5b5a005c6 Mon Sep 17 00:00:00 2001 From: Kimi <34180607+Kimi898246@users.noreply.github.com> Date: Sat, 7 May 2022 21:58:16 +0800 Subject: [PATCH 171/434] Traditional Chinese | Translation Patches yeah i fucked up that one line of translation oops also added two lines of translation too --- src/main/resources/languages/zh-TW.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 22d1339c8..2e5419290 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -57,7 +57,7 @@ "player_execute_error": "請在遊戲裡使用這條指令。", "command_exist_error": "找不到指令。", "invalid": { - "amount": "無效的 數量.", + "amount": "無效的數量。", "artifactId": "無效的聖遺物ID。", "avatarId": "無效的角色ID。", "avatarLevel": "無效的角色等級。", @@ -119,7 +119,7 @@ }, "coop": { "usage": "用法:coop <playerId> <target playerId>", - "success": "Summoned %s to %s's world." + "success": "召喚了 %s 到 %s 的世界。" }, "enter_dungeon": { "usage": "用法:enterdungeon <dungeon id>", @@ -270,7 +270,7 @@ "q_skill_id": "Q技能ID %s。" }, "teleportAll": { - "success": "Summoned all players to your location.", + "success": "召喚了所有玩家到你的位置上。", "error": "此指令僅可在多人遊戲下可用。" }, "teleport": { @@ -295,4 +295,4 @@ "available_commands": "可用指令:" } } -} \ No newline at end of file +} From 03544acb3de3af5354028f56efefd24d23f17f01 Mon Sep 17 00:00:00 2001 From: zhaodice <63996691+zhaodice@users.noreply.github.com> Date: Sat, 7 May 2022 23:19:48 +0800 Subject: [PATCH 172/434] fix issues 629 fix #629 --- src/main/resources/languages/en-US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 48ffb5b0b..fb33ba287 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -184,7 +184,7 @@ "account_error": "The account cannot be found." }, "position": { - "success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" + "success": "Coordinates: %s, %s, %s\nScene id: %s" }, "reload": { "reload_start": "Reloading config.", @@ -295,4 +295,4 @@ "available_commands": "Available commands: " } } -} \ No newline at end of file +} From fadda64699be8224ba4f0bc566f50ce700a420d6 Mon Sep 17 00:00:00 2001 From: zhaodice <63996691+zhaodice@users.noreply.github.com> Date: Sat, 7 May 2022 23:31:12 +0800 Subject: [PATCH 173/434] fix issue 635 fix #635 --- .../java/emu/grasscutter/command/commands/GiveCharCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 87c2d61e2..4b3279202 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -77,6 +77,6 @@ public final class GiveCharCommand implements CommandHandler { avatar.recalcStats(); targetPlayer.addAvatar(avatar); - CommandHandler.sendMessage(sender, translate("commands.execution.giveChar.given", Integer.toString(avatarId), Integer.toString(level), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate("commands.giveChar.given", Integer.toString(avatarId), Integer.toString(level), Integer.toString(targetPlayer.getUid()))); } } From 1445fe9ce5ca72e495efa470d0ae6f1dfa0cbcc5 Mon Sep 17 00:00:00 2001 From: Piotr Blecharski <piotr.blecharski02@gmail.com> Date: Sat, 7 May 2022 23:20:12 +0200 Subject: [PATCH 174/434] Command list with descriptions in handbook --- .DS_Store | Bin 0 -> 6148 bytes .../emu/grasscutter/command/CommandMap.java | 6 +++++ .../java/emu/grasscutter/tools/Tools.java | 23 ++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..eaf6ad184c75dd2ba42a1541ed49d325ff465cce GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O(;SR3OxqA7ObsU#Y>3w1&ruHr6#6mFlI~Am_sS#tS{t~_&m<+ zZp2arPa<{(X203_$+F*u{b7u8HxG9ivlwFrC}Pco<_p0%>XOv72a(Hf<Sb>kY|OHt zSjr~HUt|E+&V*HiHDiK(TEA=(#3F#`YcPqUY})O<@mjsHw%#<FM$5SN?nU8E{b`=L zetL<$Ga=%jvi;yZ8W$sL^H?NPKT5`_EQrD}q+DG@Nhk_e<Vl#xT2BQG(=bO?dp0}h z4LbJTpg-@}v%_8&?Ee0IZW>$LJ4dIZhxjoO&x%eCf1Z>LjTO9r@x{!ZyfleLatG0> zvZ^2?28aP-U{x5<`=8NTl?l^4i2-8Z#|+^9V1pt$8cT(8>wpHYj~H(tqJWKW2}E0? zqp?&75fH9Q0aYp2PYkZg!Eft4M`Ni_l`}3!hVST+xqhK=IXd`lna;SQkXmAZ7+7SW zuDTYU|0mzS{}+p>M+^`H|B3<L7<xk&rlil-rODx0YlGf^qF`RB@S_9_c@#q~9>r}? aDd4x!0CY5#3c&+H7Xe8FHN?QLGVl&qd`++b literal 0 HcmV?d00001 diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index 07deb84fd..a183c6ac3 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -79,6 +79,12 @@ public final class CommandMap { return this; } + public List<Command> getAnnotationsAsList() { return new LinkedList<>(this.annotations.values()); } + + public HashMap<String, Command> getAnnotations() { + return new LinkedHashMap<>(this.annotations); + } + /** * Returns a list of all registered commands. * diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 7429c143f..d9923a656 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -13,16 +13,15 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import com.google.gson.reflect.TypeToken; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.data.def.AvatarData; @@ -111,7 +110,21 @@ final class ToolsWithLanguageOption { writer.println("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator()); - + + CommandMap cmdMap = new CommandMap(true); + List<Command> cmdList = new ArrayList<>(cmdMap.getAnnotationsAsList()); + + writer.println("// Commands"); + for (Command cmd : cmdList) { + String cmdName = cmd.label(); + while (cmdName.length() <= 15) { + cmdName = " " + cmdName; + } + writer.println(cmdName + " : " + cmd.description()); + } + + writer.println(); + list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); Collections.sort(list); From 55d47a014f6c64a2736f1a960ba36a52c5b87bcb Mon Sep 17 00:00:00 2001 From: Piotr Blecharski <piotr.blecharski02@gmail.com> Date: Sat, 7 May 2022 23:21:57 +0200 Subject: [PATCH 175/434] Deleted .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index eaf6ad184c75dd2ba42a1541ed49d325ff465cce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z-O8O(;SR3OxqA7ObsU#Y>3w1&ruHr6#6mFlI~Am_sS#tS{t~_&m<+ zZp2arPa<{(X203_$+F*u{b7u8HxG9ivlwFrC}Pco<_p0%>XOv72a(Hf<Sb>kY|OHt zSjr~HUt|E+&V*HiHDiK(TEA=(#3F#`YcPqUY})O<@mjsHw%#<FM$5SN?nU8E{b`=L zetL<$Ga=%jvi;yZ8W$sL^H?NPKT5`_EQrD}q+DG@Nhk_e<Vl#xT2BQG(=bO?dp0}h z4LbJTpg-@}v%_8&?Ee0IZW>$LJ4dIZhxjoO&x%eCf1Z>LjTO9r@x{!ZyfleLatG0> zvZ^2?28aP-U{x5<`=8NTl?l^4i2-8Z#|+^9V1pt$8cT(8>wpHYj~H(tqJWKW2}E0? zqp?&75fH9Q0aYp2PYkZg!Eft4M`Ni_l`}3!hVST+xqhK=IXd`lna;SQkXmAZ7+7SW zuDTYU|0mzS{}+p>M+^`H|B3<L7<xk&rlil-rODx0YlGf^qF`RB@S_9_c@#q~9>r}? aDd4x!0CY5#3c&+H7Xe8FHN?QLGVl&qd`++b From 75032b4aa221ab21d7111691814d3e92cd174435 Mon Sep 17 00:00:00 2001 From: HotaruYS <105128850+HotaruYS@users.noreply.github.com> Date: Sat, 7 May 2022 22:07:55 +0200 Subject: [PATCH 176/434] Respect FrontHTTPS when creating URI for gacha record --- src/main/java/emu/grasscutter/game/gacha/GachaBanner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 2317af38e..b48cb0898 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -96,7 +96,7 @@ public class GachaBanner { return toProto(""); } public GachaInfo toProto(String sessionKey) { - String record = "https://" + String record = "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) From f133a8b12351d74a1817f0bc2de0367ff4d1b203 Mon Sep 17 00:00:00 2001 From: Magix <27646710+KingRainbow44@users.noreply.github.com> Date: Sat, 7 May 2022 17:58:18 -0400 Subject: [PATCH 177/434] Update PluginManager.java --- src/main/java/emu/grasscutter/plugin/PluginManager.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java index 4844a698a..bc78d12eb 100644 --- a/src/main/java/emu/grasscutter/plugin/PluginManager.java +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -52,8 +52,8 @@ public final class PluginManager { plugins.forEach(plugin -> { try { pluginNames[plugins.indexOf(plugin)] = plugin.toURI().toURL(); - } catch (MalformedURLException e) { - e.printStackTrace(); + } catch (MalformedURLException exception) { + Grasscutter.getLogger().warn("Unable to load plugin.", exception); } }); @@ -79,7 +79,7 @@ public final class PluginManager { JarEntry entry = entries.nextElement(); if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue; String className = entry.getName().replace(".class", "").replace("/", "."); - classLoader.loadClass(className); //For all plugin we use the same class loader. + classLoader.loadClass(className); // Use the same class loader for ALL plugins. } Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass); @@ -182,4 +182,4 @@ public final class PluginManager { (event.isCanceled() && handler.ignoresCanceled()) ) handler.getCallback().consume((T) event); } -} \ No newline at end of file +} From 330427f5a56e01ecc494db4e042edad17f1eec40 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Sat, 7 May 2022 18:12:53 -0400 Subject: [PATCH 178/434] Simplify the language fallback system --- src/main/java/emu/grasscutter/Config.java | 2 +- .../java/emu/grasscutter/Grasscutter.java | 5 ++- .../java/emu/grasscutter/utils/Language.java | 44 +++++-------------- .../java/emu/grasscutter/utils/Utils.java | 3 +- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 1111ba920..3982fc46b 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -23,7 +23,7 @@ public final class Config { public GameServerOptions GameServer = new GameServerOptions(); public DispatchServerOptions DispatchServer = new DispatchServerOptions(); public Locale LocaleLanguage = Locale.getDefault(); - public Locale DefaultLanguage = Locale.US; + public Locale DefaultLanguage = Locale.ENGLISH; public Boolean OpenStamina = true; public GameServerOptions getGameServerOptions() { diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 51bc3fcc5..c593f5f13 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -149,9 +149,10 @@ public final class Grasscutter { public static void loadLanguage() { var locale = config.LocaleLanguage; - String languageTag = locale.toLanguageTag(); + var languageTag = locale.toLanguageTag(); + if (languageTag.equals("und")) { - Grasscutter.getLogger().error("Illegal locale language, using en-US instead."); + Grasscutter.getLogger().error("Illegal locale language, using 'en-US' instead."); language = Language.getLanguage("en-US"); } else { language = Language.getLanguage(languageTag); diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 340d35dc2..0af77adc1 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -19,7 +19,7 @@ public final class Language { * @return A language instance. */ public static Language getLanguage(String langCode) { - return new Language(langCode + ".json"); + return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag()); } /** @@ -30,6 +30,7 @@ public final class Language { */ public static String translate(String key, Object... args) { String translated = Grasscutter.getLanguage().get(key); + try { return translated.formatted(args); } catch (Exception exception) { @@ -38,48 +39,25 @@ public final class Language { } } - /** - * creates a language instance. - * @param fileName The name of the language file. - */ - private Language(String fileName) { - @Nullable JsonObject languageData = null; - - languageData = loadLanguage(fileName); - - if (languageData == null) { - Grasscutter.getLogger().info("Now switch to default language"); - languageData = loadDefaultLanguage(); - } - - assert languageData != null : "languageData is null"; - this.languageData = languageData; - } - - /** - * Load default language file and creates a language instance. - * @return language data - */ - private JsonObject loadDefaultLanguage() { - var fileName = Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json"; - return loadLanguage(fileName); - } - /** * Reads a file and creates a language instance. * @param fileName The name of the language file. - * @return language data */ - private JsonObject loadLanguage(String fileName) { + private Language(String fileName, String fallback) { @Nullable JsonObject languageData = null; - + try { InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); + if(file == null) { + file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); + } + languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to load language file: " + fileName); + Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception); } - return languageData; + + this.languageData = languageData; } /** diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 259fc8ad5..6d11822f0 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -1,6 +1,7 @@ package emu.grasscutter.utils; import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.*; @@ -254,7 +255,7 @@ public final class Utils { */ public static String readFromInputStream(InputStream stream) { StringBuilder stringBuilder = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream,"UTF-8"))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } stream.close(); From 426f3701e815a5b2d5659368c1cddb7c3cf76458 Mon Sep 17 00:00:00 2001 From: Magix <27646710+KingRainbow44@users.noreply.github.com> Date: Sat, 7 May 2022 18:17:32 -0400 Subject: [PATCH 179/434] Update build.gradle (in prep for 1.1.0) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8c9257777..eefef5b48 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' -version = '1.0.3-dev' +version = '1.1.0' sourceCompatibility = 17 targetCompatibility = 17 @@ -228,4 +228,4 @@ javadoc { processResources { dependsOn "generateProto" -} \ No newline at end of file +} From fbaeaee4b5aa82fe10897b60ea642d4428e8abd8 Mon Sep 17 00:00:00 2001 From: Kimi <34180607+Kimi898246@users.noreply.github.com> Date: Sun, 8 May 2022 06:25:49 +0800 Subject: [PATCH 180/434] another translation patches because i fucked it up i hate myself --- src/main/resources/languages/zh-TW.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 2e5419290..7545df6e9 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -127,7 +127,7 @@ "not_found_error": "此副本不存在。", "in_dungeon_error": "你已經在祕境中了。" }, - "giveAll": { + "chAll": { "usage": "用法:giveall [player] [amount]", "started": "正在賦予全部物品...", "success": "已賦予全部物品。", @@ -140,7 +140,7 @@ }, "giveChar": { "usage": "用法:givechar <player> <itemId|itemName> [amount]", - "given": "Given %s with level %s to %s.", + "given": "已將 %s 等級 %s 給予 %s。", "invalid_avatar_id": "無效的角色ID。", "invalid_avatar_level": "無效的角色等級。.", "invalid_avatar_or_player_id": "無效的角色ID/玩家ID。" From eb4dabe162930e6bb2c2e67cd4bccb66b8e2bd65 Mon Sep 17 00:00:00 2001 From: Kimi <34180607+Kimi898246@users.noreply.github.com> Date: Sun, 8 May 2022 06:47:58 +0800 Subject: [PATCH 181/434] Update zh-TW.json --- src/main/resources/languages/zh-TW.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 7545df6e9..49e0f256d 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -127,7 +127,7 @@ "not_found_error": "此副本不存在。", "in_dungeon_error": "你已經在祕境中了。" }, - "chAll": { + "giveAll": { "usage": "用法:giveall [player] [amount]", "started": "正在賦予全部物品...", "success": "已賦予全部物品。", From 090b00556bcde8cf43bcea80d5294f5e49b2f2ca Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 05:00:50 -0700 Subject: [PATCH 182/434] More reliable stamina calculation by separately handling immediate one-time cost and cost over time. --- proto/WorldPlayerReviveReq.proto | 2 +- .../MovementManager/MovementManager.java | 428 ++++++++---------- .../emu/grasscutter/game/player/Player.java | 2 +- .../grasscutter/game/player/TeamManager.java | 2 +- .../recv/HandlerCombatInvocationsNotify.java | 76 +++- .../recv/HandlerEvtDoSkillSuccNotify.java | 6 +- 6 files changed, 269 insertions(+), 247 deletions(-) diff --git a/proto/WorldPlayerReviveReq.proto b/proto/WorldPlayerReviveReq.proto index edaeab683..d9ea86c1c 100644 --- a/proto/WorldPlayerReviveReq.proto +++ b/proto/WorldPlayerReviveReq.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option csharp_namespace = "YSFreedom.Common.Protocol"; +option java_package = "emu.grasscutter.net.proto"; message WorldPlayerReviveReq { enum CmdId { diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java index 23b45903a..ece02a0fb 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java @@ -7,22 +7,31 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; +import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; +import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; -import emu.grasscutter.net.proto.VectorOuterClass; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.Position; -import org.jetbrains.annotations.NotNull; import java.lang.Math; import java.util.*; public class MovementManager { - - public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); + private final Player player; + private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); + private Position currentCoordinates = new Position(0, 0, 0); + private Position previousCoordinates = new Position(0, 0, 0); + private MotionState currentState = MotionState.MOTION_STANDBY; + private MotionState previousState = MotionState.MOTION_STANDBY; + private Timer sustainedStaminaHandlerTimer; + private GameSession cachedSession = null; + private GameEntity cachedEntity = null; + private int staminaRecoverDelay = 0; + private boolean isInSkillMove = false; private enum ConsumptionType { None(0), @@ -31,8 +40,8 @@ public class MovementManager { CLIMB_START(-500), CLIMBING(-150), CLIMB_JUMP(-2500), - DASH(-1800), - SPRINT(-360), + SPRINT(-1800), + DASH(-360), FLY(-60), SWIM_DASH_START(-200), SWIM_DASH(-200), @@ -47,6 +56,7 @@ public class MovementManager { POWERED_FLY(500); public final int amount; + ConsumptionType(int amount) { this.amount = amount; } @@ -55,33 +65,26 @@ public class MovementManager { private class Consumption { public ConsumptionType consumptionType; public int amount; + public Consumption(ConsumptionType ct, int a) { consumptionType = ct; amount = a; } + public Consumption(ConsumptionType ct) { this(ct, ct.amount); } } - private MotionState previousState = MotionState.MOTION_STANDBY; - private MotionState currentState = MotionState.MOTION_STANDBY; - private Position previousCoordinates = new Position(0, 0, 0); - private Position currentCoordinates = new Position(0, 0, 0); + public boolean getIsInSkillMove() { + return isInSkillMove; + } - private final Player player; - - private float landSpeed = 0; - private long landTimeMillisecond = 0; - private Timer movementManagerTickTimer; - private GameSession cachedSession = null; - private GameEntity cachedEntity = null; - private int staminaRecoverDelay = 0; - private int skillCaster = 0; - private int skillCasting = 0; + public void setIsInSkillMove(boolean b) { + isInSkillMove = b; + } public MovementManager(Player player) { - previousCoordinates.add(new Position(0,0,0)); this.player = player; MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( @@ -129,252 +132,225 @@ public class MovementManager { ))); MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( - MotionState.MOTION_FIGHT + MotionState.MOTION_FIGHT ))); - - - } - - public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) { - if (movementManagerTickTimer == null) { - movementManagerTickTimer = new Timer(); - movementManagerTickTimer.scheduleAtFixedRate(new MotionManagerTick(), 0, 200); - } - // cache info for later use in tick - cachedSession = session; - cachedEntity = entity; - - MotionInfo motionInfo = moveInfo.getMotionInfo(); - moveEntity(entity, moveInfo); - VectorOuterClass.Vector posVector = motionInfo.getPos(); - Position newPos = new Position(posVector.getX(), - posVector.getY(), posVector.getZ());; - if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { - currentCoordinates = newPos; - } - currentState = motionInfo.getState(); - Grasscutter.getLogger().debug("" + currentState + "\t" + (moveInfo.getIsReliable() ? "reliable" : "")); - handleFallOnGround(motionInfo); - } - - public void resetTimer() { - Grasscutter.getLogger().debug("MovementManager ticker stopped"); - movementManagerTickTimer.cancel(); - movementManagerTickTimer = null; - } - - private void moveEntity(GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { - entity.getPosition().set(moveInfo.getMotionInfo().getPos()); - entity.getRotation().set(moveInfo.getMotionInfo().getRot()); - entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); - entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); - entity.setMotionState(moveInfo.getMotionInfo().getState()); } private boolean isPlayerMoving() { float diffX = currentCoordinates.getX() - previousCoordinates.getX(); float diffY = currentCoordinates.getY() - previousCoordinates.getY(); float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); - // Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); - return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2; + Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + + ", " + diffX + ", " + diffY + ", " + diffZ); + return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; } - private int getCurrentStamina() { - return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); - } - - private int getMaximumStamina() { - return player.getProperty(PlayerProperty.PROP_MAX_STAMINA); - } - - // Returns new stamina - public int updateStamina(GameSession session, int amount) { - int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); - if (amount == 0) { + // Returns new stamina and sends PlayerPropNotify + public int updateStamina(GameSession session, Consumption consumption) { + int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + if (consumption.amount == 0) { return currentStamina; } - int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); - int newStamina = currentStamina + amount; + int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); + Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," + + consumption.amount + ")"); + int newStamina = currentStamina + consumption.amount; if (newStamina < 0) { newStamina = 0; } if (newStamina > playerMaxStamina) { newStamina = playerMaxStamina; } - session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); return newStamina; } - private void handleFallOnGround(@NotNull MotionInfo motionInfo) { - MotionState state = motionInfo.getState(); - // land speed and fall on ground event arrive in different packets - // cache land speed - if (state == MotionState.MOTION_LAND_SPEED) { - landSpeed = motionInfo.getSpeed().getY(); - landTimeMillisecond = System.currentTimeMillis(); + // Kills avatar, removes entity and sends notification. + // TODO: Probably move this to Avatar class? since other components may also need to kill avatar. + public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) { + session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(), + LifeState.LIFE_DEAD, dieType)); + session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType)); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); + player.getScene().removeEntity(entity); + ((EntityAvatar) entity).onDeath(dieType, 0); + } + + public void startSustainedStaminaHandler() { + if (sustainedStaminaHandlerTimer == null) { + sustainedStaminaHandlerTimer = new Timer(); + sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); + Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); } - if (state == MotionState.MOTION_FALL_ON_GROUND) { - // if not received immediately after MOTION_LAND_SPEED, discard this packet. - // TODO: Test in high latency. - int maxDelay = 200; - if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) { - Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard."); - return; + } + + public void stopSustainedStaminaHandler() { + Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); + sustainedStaminaHandlerTimer.cancel(); + sustainedStaminaHandlerTimer = null; + } + + // Handlers + + // External trigger handler + + public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) { + handleImmediateStamina(session, notify); + } + + public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) { + // cache info for later use in SustainedStaminaHandler tick + cachedSession = session; + cachedEntity = entity; + MotionInfo motionInfo = moveInfo.getMotionInfo(); + MotionState motionState = motionInfo.getState(); + boolean isReliable = moveInfo.getIsReliable(); + Grasscutter.getLogger().trace("" + motionState + "\t" + (isReliable ? "reliable" : "")); + if (isReliable) { + currentState = motionState; + Vector posVector = motionInfo.getPos(); + Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); + if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { + currentCoordinates = newPos; } - float currentHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - float maxHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float damage = 0; - Grasscutter.getLogger().debug("LandSpeed: " + landSpeed); - if (landSpeed < -23.5) { - damage = (float)(maxHP * 0.33); + } + startSustainedStaminaHandler(); + handleImmediateStamina(session, motionInfo, motionState, entity); + } + + // Internal handler + + private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState, + GameEntity entity) { + switch (motionState) { + case MOTION_DASH_BEFORE_SHAKE: + if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { + updateStamina(session, new Consumption(ConsumptionType.SPRINT)); + } + break; + case MOTION_CLIMB_JUMP: + if (previousState != MotionState.MOTION_CLIMB_JUMP) { + updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP)); + } + break; + case MOTION_SWIM_DASH: + if (previousState != MotionState.MOTION_SWIM_DASH) { + updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START)); + } + break; + } + } + + private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) { + Consumption consumption = getFightConsumption(notify.getSkillId()); + updateStamina(session, consumption); + } + + private class SustainedStaminaHandler extends TimerTask { + public void run() { + if (Grasscutter.getConfig().OpenStamina) { + boolean moving = isPlayerMoving(); + int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); + if (moving || (currentStamina < maxStamina)) { + Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + + (currentStamina >= maxStamina) + ", recalculate stamina"); + Consumption consumption = new Consumption(ConsumptionType.None); + if (!isInSkillMove) { + if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { + consumption = getClimbSustainedConsumption(); + } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { + consumption = getSwimSustainedConsumptions(); + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + consumption = getRunWalkDashSustainedConsumption(); + } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { + consumption = getFlySustainedConsumption(); + } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { + consumption = getStandSustainedConsumption(); + } + } + if (cachedSession != null) { + if (consumption.amount < 0) { + staminaRecoverDelay = 0; + } + if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { + // For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. + if (staminaRecoverDelay < 10) { + // For others recover after 2 seconds (10 ticks) - as official server does. + staminaRecoverDelay++; + consumption = new Consumption(ConsumptionType.None); + } + } + updateStamina(cachedSession, consumption); + } + handleDrowning(); + } } - if (landSpeed < -25) { - damage = (float)(maxHP * 0.5); - } - if (landSpeed < -26.5) { - damage = (float)(maxHP * 0.66); - } - if (landSpeed < -28) { - damage = (maxHP * 1); - } - float newHP = currentHP - damage; - if (newHP < 0) { - newHP = 0; - } - Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); - cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); - cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); - if (newHP == 0) { - killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_FALL); - } - landSpeed = 0; + previousState = currentState; + previousCoordinates = new Position( + currentCoordinates.getX(), + currentCoordinates.getY(), + currentCoordinates.getZ() + ); } } private void handleDrowning() { - int stamina = getCurrentStamina(); + int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); if (stamina < 10) { boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState); - Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming); + Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming); if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) { killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); } } } - public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) { - cachedSession.send(new PacketAvatarLifeStateChangeNotify( - cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), - LifeState.LIFE_DEAD, - dieType - )); - cachedSession.send(new PacketLifeStateChangeNotify( - cachedEntity, - LifeState.LIFE_DEAD, - dieType - )); - cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0); - cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); - entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); - session.getPlayer().getScene().removeEntity(entity); - ((EntityAvatar)entity).onDeath(dieType, 0); - } + // Consumption Calculators - private class MotionManagerTick extends TimerTask - { - public void run() { - if (Grasscutter.getConfig().OpenStamina) { - boolean moving = isPlayerMoving(); - if (moving || (getCurrentStamina() < getMaximumStamina())) { - // Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); - Consumption consumption = new Consumption(ConsumptionType.None); - - // TODO: refactor these conditions. - if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - consumption = getClimbConsumption(); - } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { - consumption = getSwimConsumptions(); - } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - consumption = getRunWalkDashConsumption(); - } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - consumption = getFlyConsumption(); - } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - consumption = getStandConsumption(); - } else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) { - consumption = getFightConsumption(); - } - - // delay 2 seconds before start recovering - as official server does. - if (cachedSession != null) { - if (consumption.amount < 0) { - staminaRecoverDelay = 0; - } - if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { - if (staminaRecoverDelay < 10) { - staminaRecoverDelay++; - consumption = new Consumption(ConsumptionType.None); - } - } - // Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); - updateStamina(cachedSession, consumption.amount); - } - - // tick triggered - handleDrowning(); - } - } - - previousState = currentState; - previousCoordinates = new Position(currentCoordinates.getX(), - currentCoordinates.getY(), currentCoordinates.getZ());; - } - } - - private Consumption getClimbConsumption() { + private Consumption getFightConsumption(int skillCasting) { Consumption consumption = new Consumption(ConsumptionType.None); - if (currentState == MotionState.MOTION_CLIMB) { + HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ + put(10013, -1000); // Kamisato Ayaka + put(10413, -1000); // Mona + }}; + if (fightingCost.containsKey(skillCasting)) { + consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting)); + } + return consumption; + } + + private Consumption getClimbSustainedConsumption() { + Consumption consumption = new Consumption(ConsumptionType.None); + if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) { consumption = new Consumption(ConsumptionType.CLIMBING); if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { consumption = new Consumption(ConsumptionType.CLIMB_START); } - if (!isPlayerMoving()) { - consumption = new Consumption(ConsumptionType.None); - } - } - if (currentState == MotionState.MOTION_CLIMB_JUMP) { - if (previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = new Consumption(ConsumptionType.CLIMB_JUMP); - } } return consumption; } - private Consumption getSwimConsumptions() { + private Consumption getSwimSustainedConsumptions() { Consumption consumption = new Consumption(ConsumptionType.None); if (currentState == MotionState.MOTION_SWIM_MOVE) { consumption = new Consumption(ConsumptionType.SWIMMING); } if (currentState == MotionState.MOTION_SWIM_DASH) { - consumption = new Consumption(ConsumptionType.SWIM_DASH_START); - if (previousState == MotionState.MOTION_SWIM_DASH) { - consumption = new Consumption(ConsumptionType.SWIM_DASH); - } + consumption = new Consumption(ConsumptionType.SWIM_DASH); } return consumption; } - private Consumption getRunWalkDashConsumption() { + private Consumption getRunWalkDashSustainedConsumption() { Consumption consumption = new Consumption(ConsumptionType.None); - if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) { - consumption = new Consumption(ConsumptionType.DASH); - if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) { - // only charge once - consumption = new Consumption(ConsumptionType.SPRINT); - } - } if (currentState == MotionState.MOTION_DASH) { - consumption = new Consumption(ConsumptionType.SPRINT); + consumption = new Consumption(ConsumptionType.DASH); } if (currentState == MotionState.MOTION_RUN) { consumption = new Consumption(ConsumptionType.RUN); @@ -385,22 +361,21 @@ public class MovementManager { return consumption; } - private Consumption getFlyConsumption() { + private Consumption getFlySustainedConsumption() { Consumption consumption = new Consumption(ConsumptionType.FLY); HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ put(212301, 0.8f); // Amber put(222301, 0.8f); // Venti }}; float reduction = 1; - for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) { - for (int skillId: entity.getAvatar().getProudSkillList()) { + for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) { + for (int skillId : entity.getAvatar().getProudSkillList()) { if (glidingCostReduction.containsKey(skillId)) { reduction = glidingCostReduction.get(skillId); } } } consumption.amount *= reduction; - // POWERED_FLY, e.g. wind tunnel if (currentState == MotionState.MOTION_POWERED_FLY) { consumption = new Consumption(ConsumptionType.POWERED_FLY); @@ -408,7 +383,7 @@ public class MovementManager { return consumption; } - private Consumption getStandConsumption() { + private Consumption getStandSustainedConsumption() { Consumption consumption = new Consumption(ConsumptionType.None); if (currentState == MotionState.MOTION_STANDBY) { consumption = new Consumption(ConsumptionType.STANDBY); @@ -418,25 +393,4 @@ public class MovementManager { } return consumption; } - - private Consumption getFightConsumption() { - Consumption consumption = new Consumption(ConsumptionType.None); - HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ - put(10013, -1000); // Kamisato Ayaka - put(10413, -1000); // Mona - }}; - if (fightingCost.containsKey(skillCasting)) { - consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting)); - // only handle once, so reset. - skillCasting = 0; - skillCaster = 0; - } - return consumption; - } - - public void notifySkill(int caster, int skillId) { - skillCaster = caster; - skillCasting = skillId; - } } - diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 1eb5e3526..d2ce39d1c 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -1152,7 +1152,7 @@ public class Player { public void onLogout() { // stop stamina calculation - getMovementManager().resetTimer(); + getMovementManager().stopSustainedStaminaHandler(); // force to leave the dungeon if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) { diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 16e8942ad..775be2b87 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -557,7 +557,7 @@ public class TeamManager { // return; // } // } - player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn + player.getMovementManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn // Revive all team members for (EntityAvatar entity : getActiveTeam()) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 27e4ca6ff..c7bbccc5e 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -1,6 +1,8 @@ package emu.grasscutter.server.packet.recv; +import emu.grasscutter.Grasscutter; import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; @@ -8,11 +10,21 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; + +import java.util.HashMap; @Opcodes(PacketOpcodes.CombatInvocationsNotify) public class HandlerCombatInvocationsNotify extends PacketHandler { + private float cachedLandingSpeed = 0; + private long cachedLandingTimeMillisecond = 0; + private boolean monitorLandingEvent = false; + @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload); @@ -28,7 +40,33 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); if (entity != null) { - session.getPlayer().getMovementManager().handle(session, moveInfo, entity); + // Move player + MotionInfo motionInfo = moveInfo.getMotionInfo(); + entity.getPosition().set(motionInfo.getPos()); + entity.getRotation().set(motionInfo.getRot()); + entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); + entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); + MotionState motionState = motionInfo.getState(); + entity.setMotionState(motionState); + + session.getPlayer().getMovementManager().handleCombatInvocationsNotify(session, moveInfo, entity); + + // TODO: handle MOTION_FIGHT landing + // For plunge attacks, LAND_SPEED is always -30 and is not useful. + // May need the height when starting plunge attack. + + if (monitorLandingEvent) { + if (motionState == MotionState.MOTION_FALL_ON_GROUND) { + monitorLandingEvent = false; + handleFallOnGround(session, entity, motionState); + } + } + if (motionState == MotionState.MOTION_LAND_SPEED) { + // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packet. Cache land speed for later use. + cachedLandingSpeed = motionInfo.getSpeed().getY(); + cachedLandingTimeMillisecond = System.currentTimeMillis(); + monitorLandingEvent = true; + } } break; default: @@ -47,5 +85,39 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { } } - + private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { + // If not received immediately after MOTION_LAND_SPEED, discard this packet. + int maxDelay = 200; + long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond; + Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : "")); + if (actualDelay > maxDelay) { + return; + } + float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float damage = 0; + if (cachedLandingSpeed < -23.5) { + damage = (float) (maxHP * 0.33); + } + if (cachedLandingSpeed < -25) { + damage = (float) (maxHP * 0.5); + } + if (cachedLandingSpeed < -26.5) { + damage = (float) (maxHP * 0.66); + } + if (cachedLandingSpeed < -28) { + damage = (maxHP * 1); + } + float newHP = currentHP - damage; + if (newHP < 0) { + newHP = 0; + } + Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP); + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + if (newHP == 0) { + session.getPlayer().getMovementManager().killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_FALL); + } + cachedLandingSpeed = 0; + } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java index a57ae9665..6a08693bb 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -1,6 +1,5 @@ package emu.grasscutter.server.packet.recv; -import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; @@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); // TODO: Will be used for deducting stamina for charged skills. - int caster = notify.getCasterId(); - int skillId = notify.getSkillId(); - - session.getPlayer().getMovementManager().notifySkill(caster, skillId); + session.getPlayer().getMovementManager().handleEvtDoSkillSuccNotify(session, notify); } } From 032db81e07c9f1cb16f92d158914f9299f52f0c5 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 16:29:40 -0700 Subject: [PATCH 183/434] Stop stamina consumption on game pause --- .../{SotSManager => }/SotSManager.java | 7 +++-- ...vementManager.java => StaminaManager.java} | 19 ++++++++----- .../emu/grasscutter/game/player/Player.java | 27 +++++++++---------- .../grasscutter/game/player/TeamManager.java | 2 +- .../recv/HandlerCombatInvocationsNotify.java | 6 ++--- .../HandlerEnterTransPointRegionNotify.java | 11 +------- .../recv/HandlerEvtDoSkillSuccNotify.java | 2 +- .../HandlerExitTransPointRegionNotify.java | 2 +- 8 files changed, 33 insertions(+), 43 deletions(-) rename src/main/java/emu/grasscutter/game/managers/{SotSManager => }/SotSManager.java (95%) rename src/main/java/emu/grasscutter/game/managers/{MovementManager/MovementManager.java => StaminaManager.java} (94%) diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager.java similarity index 95% rename from src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java rename to src/main/java/emu/grasscutter/game/managers/SotSManager.java index 0bfdf9454..ed67c6a62 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager.java @@ -1,14 +1,11 @@ -package emu.grasscutter.game.managers.SotSManager; +package emu.grasscutter.game.managers; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.managers.MovementManager.MovementManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.World; import emu.grasscutter.net.proto.ChangeHpReasonOuterClass; import emu.grasscutter.net.proto.PropChangeReasonOuterClass; import emu.grasscutter.server.game.GameSession; @@ -29,6 +26,8 @@ public class SotSManager { private final Player player; private Timer autoRecoverTimer; + public final static int GlobalMaximumSpringVolume = 8500000; + public SotSManager(Player player) { this.player = player; } diff --git a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager.java similarity index 94% rename from src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java rename to src/main/java/emu/grasscutter/game/managers/StaminaManager.java index ece02a0fb..5fd4f57b4 100644 --- a/src/main/java/emu/grasscutter/game/managers/MovementManager/MovementManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager.java @@ -1,4 +1,4 @@ -package emu.grasscutter.game.managers.MovementManager; +package emu.grasscutter.game.managers; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.entity.EntityAvatar; @@ -20,9 +20,11 @@ import emu.grasscutter.utils.Position; import java.lang.Math; import java.util.*; -public class MovementManager { +public class StaminaManager { private final Player player; private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); + + public final static int GlobalMaximumStamina = 24000; private Position currentCoordinates = new Position(0, 0, 0); private Position previousCoordinates = new Position(0, 0, 0); private MotionState currentState = MotionState.MOTION_STANDBY; @@ -84,7 +86,7 @@ public class MovementManager { isInSkillMove = b; } - public MovementManager(Player player) { + public StaminaManager(Player player) { this.player = player; MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( @@ -181,11 +183,14 @@ public class MovementManager { } public void startSustainedStaminaHandler() { - if (sustainedStaminaHandlerTimer == null) { - sustainedStaminaHandlerTimer = new Timer(); - sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); - Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); + if (!player.isPaused()) { + if (sustainedStaminaHandlerTimer == null) { + sustainedStaminaHandlerTimer = new Timer(); + sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); + Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); + } } + } public void stopSustainedStaminaHandler() { diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index d2ce39d1c..3a2f3cbff 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -22,8 +22,8 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.MailHandler; -import emu.grasscutter.game.managers.MovementManager.MovementManager; -import emu.grasscutter.game.managers.SotSManager.SotSManager; +import emu.grasscutter.game.managers.StaminaManager; +import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; @@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue; @Entity(value = "players", useDiscriminator = false) public class Player { - @Transient private static int GlobalMaximumSpringVolume = 8500000; - @Transient private static int GlobalMaximumStamina = 24000; - @Id private int id; @Indexed(options = @IndexOptions(unique = true)) private String accountId; @@ -132,7 +129,7 @@ public class Player { @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; private MapMarksManager mapMarksManager; - @Transient private MovementManager movementManager; + @Transient private StaminaManager staminaManager; private long springLastUsed; @@ -178,7 +175,7 @@ public class Player { this.expeditionInfo = new HashMap<>(); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); - this.movementManager = new MovementManager(this); + this.staminaManager = new StaminaManager(this); this.sotsManager = new SotSManager(this); } @@ -206,7 +203,7 @@ public class Player { this.getRotation().set(0, 307, 0); this.messageHandler = null; this.mapMarksManager = new MapMarksManager(); - this.movementManager = new MovementManager(this); + this.staminaManager = new StaminaManager(this); this.sotsManager = new SotSManager(this); } @@ -875,11 +872,11 @@ public class Player { } public void onPause() { - + staminaManager.stopSustainedStaminaHandler(); } public void onUnpause() { - + staminaManager.startSustainedStaminaHandler(); } public void sendPacket(BasePacket packet) { @@ -1024,7 +1021,7 @@ public class Player { return mapMarksManager; } - public MovementManager getMovementManager() { return movementManager; } + public StaminaManager getStaminaManager() { return staminaManager; } public SotSManager getSotSManager() { return sotsManager; } @@ -1152,7 +1149,7 @@ public class Player { public void onLogout() { // stop stamina calculation - getMovementManager().stopSustainedStaminaHandler(); + getStaminaManager().stopSustainedStaminaHandler(); // force to leave the dungeon if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) { @@ -1214,7 +1211,7 @@ public class Player { } else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001 // TODO: implement sanity check } else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002 - if (!(value >= 0 && value <= GlobalMaximumSpringVolume)) { return false; } + if (!(value >= 0 && value <= getSotSManager().GlobalMaximumSpringVolume)) { return false; } } else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003 int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME); if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; } @@ -1231,7 +1228,7 @@ public class Player { } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 if (!(0 <= value && value <= 1)) { return false; } } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 - if (!(value >= 0 && value <= GlobalMaximumStamina)) { return false; } + if (!(value >= 0 && value <= getStaminaManager().GlobalMaximumStamina)) { return false; } } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } @@ -1242,7 +1239,7 @@ public class Player { } else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014 if (!(0 <= value)) { return false; } } else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015 - // see 10015 + // see PlayerProperty.PROP_PLAYER_HCOIN comments } else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016 // See 10015 } else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017 diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 775be2b87..204af2976 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -557,7 +557,7 @@ public class TeamManager { // return; // } // } - player.getMovementManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn + player.getStaminaManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn // Revive all team members for (EntityAvatar entity : getActiveTeam()) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index c7bbccc5e..cc9e7b345 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -16,8 +16,6 @@ import emu.grasscutter.net.proto.PlayerDieTypeOuterClass; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import java.util.HashMap; - @Opcodes(PacketOpcodes.CombatInvocationsNotify) public class HandlerCombatInvocationsNotify extends PacketHandler { @@ -49,7 +47,7 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { MotionState motionState = motionInfo.getState(); entity.setMotionState(motionState); - session.getPlayer().getMovementManager().handleCombatInvocationsNotify(session, moveInfo, entity); + session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity); // TODO: handle MOTION_FIGHT landing // For plunge attacks, LAND_SPEED is always -30 and is not useful. @@ -116,7 +114,7 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); if (newHP == 0) { - session.getPlayer().getMovementManager().killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_FALL); + session.getPlayer().getStaminaManager().killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_FALL); } cachedLandingSpeed = 0; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java index 5591607fe..94c9bfd8b 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java @@ -1,20 +1,11 @@ package emu.grasscutter.server.packet.recv; -import emu.grasscutter.game.managers.SotSManager.SotSManager; +import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; -import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; - -import java.util.List; @Opcodes(PacketOpcodes.EnterTransPointRegionNotify) public class HandlerEnterTransPointRegionNotify extends PacketHandler { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java index 6a08693bb..705341fa0 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -14,7 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); // TODO: Will be used for deducting stamina for charged skills. - session.getPlayer().getMovementManager().handleEvtDoSkillSuccNotify(session, notify); + session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, notify); } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java index 35ec957cb..0d35c1762 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java @@ -1,6 +1,6 @@ package emu.grasscutter.server.packet.recv; -import emu.grasscutter.game.managers.SotSManager.SotSManager; +import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; From a7c1f8557901474570093a1543e3c2d04b05fb3f Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 18:07:44 -0700 Subject: [PATCH 184/434] Make stamina consumption classes public so others can use. --- .../managers/StaminaManager/Consumption.java | 15 +++++++ .../StaminaManager/ConsumptionType.java | 30 +++++++++++++ .../{ => StaminaManager}/StaminaManager.java | 42 +------------------ .../emu/grasscutter/game/player/Player.java | 2 +- 4 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java rename src/main/java/emu/grasscutter/game/managers/{ => StaminaManager}/StaminaManager.java (92%) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java new file mode 100644 index 000000000..23eb44be9 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java @@ -0,0 +1,15 @@ +package emu.grasscutter.game.managers.StaminaManager; + +public class Consumption { + public ConsumptionType consumptionType; + public int amount; + + public Consumption(ConsumptionType ct, int a) { + consumptionType = ct; + amount = a; + } + + public Consumption(ConsumptionType ct) { + this(ct, ct.amount); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java new file mode 100644 index 000000000..9a2d8ae24 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java @@ -0,0 +1,30 @@ +package emu.grasscutter.game.managers.StaminaManager; + +public enum ConsumptionType { + None(0), + + // consume + CLIMB_START(-500), + CLIMBING(-150), + CLIMB_JUMP(-2500), + SPRINT(-1800), + DASH(-360), + FLY(-60), + SWIM_DASH_START(-200), + SWIM_DASH(-200), + SWIMMING(-80), + FIGHT(0), + + // restore + STANDBY(500), + RUN(500), + WALK(500), + STANDBY_MOVE(500), + POWERED_FLY(500); + + public final int amount; + + ConsumptionType(int amount) { + this.amount = amount; + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java similarity index 92% rename from src/main/java/emu/grasscutter/game/managers/StaminaManager.java rename to src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 5fd4f57b4..3d55e5edf 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -1,4 +1,4 @@ -package emu.grasscutter.game.managers; +package emu.grasscutter.game.managers.StaminaManager; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.entity.EntityAvatar; @@ -35,48 +35,8 @@ public class StaminaManager { private int staminaRecoverDelay = 0; private boolean isInSkillMove = false; - private enum ConsumptionType { - None(0), - // consume - CLIMB_START(-500), - CLIMBING(-150), - CLIMB_JUMP(-2500), - SPRINT(-1800), - DASH(-360), - FLY(-60), - SWIM_DASH_START(-200), - SWIM_DASH(-200), - SWIMMING(-80), - FIGHT(0), - // restore - STANDBY(500), - RUN(500), - WALK(500), - STANDBY_MOVE(500), - POWERED_FLY(500); - - public final int amount; - - ConsumptionType(int amount) { - this.amount = amount; - } - } - - private class Consumption { - public ConsumptionType consumptionType; - public int amount; - - public Consumption(ConsumptionType ct, int a) { - consumptionType = ct; - amount = a; - } - - public Consumption(ConsumptionType ct) { - this(ct, ct.amount); - } - } public boolean getIsInSkillMove() { return isInSkillMove; diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 3a2f3cbff..b6956153c 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -22,7 +22,7 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.MailHandler; -import emu.grasscutter.game.managers.StaminaManager; +import emu.grasscutter.game.managers.StaminaManager.StaminaManager; import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; From 8739277970ba2f7c2d0f7c430ac11aac40de445e Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 21:47:13 +0800 Subject: [PATCH 185/434] Monsters tide turn by turn && Ban User Skill && Lua functions --- proto/CanUseSkillNotify.proto | 15 ++ .../game/entity/EntityMonster.java | 1 + .../grasscutter/game/tower/TowerManager.java | 4 +- .../emu/grasscutter/game/world/Scene.java | 8 +- .../scripts/SceneScriptManager.java | 151 +++++++++++------- .../emu/grasscutter/scripts/ScriptLib.java | 123 +++++++++++--- .../grasscutter/scripts/data/SceneGroup.java | 12 +- .../packet/send/PacketCanUseSkillNotify.java | 19 +++ src/main/resources/logback.xml | 2 + 9 files changed, 247 insertions(+), 88 deletions(-) create mode 100644 proto/CanUseSkillNotify.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java diff --git a/proto/CanUseSkillNotify.proto b/proto/CanUseSkillNotify.proto new file mode 100644 index 000000000..60ac6d7f0 --- /dev/null +++ b/proto/CanUseSkillNotify.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CanUseSkillNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 1019; + } + + bool is_can_use_skill = 1; +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java index c9d0c0982..0ae6f356b 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java @@ -117,6 +117,7 @@ public class EntityMonster extends GameEntity { this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); } if (getScene().getScriptManager().isInit() && this.getGroupId() > 0) { + getScene().getScriptManager().onMonsterDie(); getScene().getScriptManager().callEvent(EventType.EVENT_ANY_MONSTER_DIE, null); } if (getScene().getChallenge() != null && getScene().getChallenge().getGroup().id == this.getGroupId()) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 51f840663..409549a1f 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -7,6 +7,7 @@ import emu.grasscutter.data.def.TowerLevelData; import emu.grasscutter.game.dungeons.DungeonSettleListener; import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; @@ -75,7 +76,8 @@ public class TowerManager { player.getScene().setPrevScenePoint(enterPointId); player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, currentLevel)); - + // stop using skill + player.getSession().send(new PacketCanUseSkillNotify(false)); } public void notifyCurLevelRecordChange(){ diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 97099c9b9..82ce9139f 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -105,7 +105,13 @@ public class Scene { public GameEntity getEntityById(int id) { return this.entities.get(id); } - + + public GameEntity getEntityByConfigId(int configId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId) + .findFirst() + .orElse(null); + } /** * @return the autoCloseTime */ diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 5f6a1b7e6..b8ba800a6 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -1,19 +1,14 @@ package emu.grasscutter.scripts; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptException; -import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; import org.luaj.vm2.lib.jse.CoerceJavaToLua; @@ -23,12 +18,8 @@ import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.WorldLevelData; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.constants.ScriptGadgetState; -import emu.grasscutter.scripts.constants.ScriptRegionShape; import emu.grasscutter.scripts.data.SceneBlock; import emu.grasscutter.scripts.data.SceneConfig; import emu.grasscutter.scripts.data.SceneGadget; @@ -56,7 +47,12 @@ public class SceneScriptManager { private final Int2ObjectOpenHashMap<Set<SceneTrigger>> triggers; private final Int2ObjectOpenHashMap<SceneRegion> regions; - + private SceneGroup currentGroup; + private AtomicInteger monsterAlive; + private AtomicInteger monsterTideCount; + private int monsterSceneLimit; + private ConcurrentLinkedQueue<Integer> monsterOrders; + public SceneScriptManager(Scene scene) { this.scene = scene; this.scriptLib = new ScriptLib(this); @@ -222,7 +218,8 @@ public class SceneScriptManager { cs.eval(getBindings()); // Set - group.monsters = ScriptLoader.getSerializer().toList(SceneMonster.class, bindings.get("monsters")); + group.monsters = ScriptLoader.getSerializer().toList(SceneMonster.class, bindings.get("monsters")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y)); group.gadgets = ScriptLoader.getSerializer().toList(SceneGadget.class, bindings.get("gadgets")); group.triggers = ScriptLoader.getSerializer().toList(SceneTrigger.class, bindings.get("triggers")); group.suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites")); @@ -235,7 +232,7 @@ public class SceneScriptManager { // Add monsters to suite TODO optimize Int2ObjectMap<Object> map = new Int2ObjectOpenHashMap<>(); - group.monsters.forEach(m -> map.put(m.config_id, m)); + group.monsters.entrySet().forEach(m -> map.put(m.getValue().config_id, m)); group.gadgets.forEach(m -> map.put(m.config_id, m)); for (SceneSuite suite : group.suites) { @@ -323,60 +320,92 @@ public class SceneScriptManager { } public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { - spawnMonstersInGroup(group, group.getSuiteByIndex(suiteIndex)); + this.currentGroup = group; + this.monsterSceneLimit = 0; + var suite = group.getSuiteByIndex(suiteIndex); + if(suite == null){ + return; + } + suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); } public void spawnMonstersInGroup(SceneGroup group) { - spawnMonstersInGroup(group, null); + this.currentGroup = group; + this.monsterSceneLimit = 0; + group.monsters.values().forEach(mob -> spawnMonstersInGroup(group, mob)); } - - public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { - List<SceneMonster> monsters = group.monsters; - - if (suite != null) { - monsters = suite.sceneMonsters; + public void spawnMonstersInGroup(SceneGroup group,Integer[] ordersConfigId, int tideCount, int sceneLimit) { + this.currentGroup = group; + this.monsterSceneLimit = sceneLimit; + this.monsterTideCount = new AtomicInteger(tideCount); + this.monsterAlive = new AtomicInteger(0); + this.monsterOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); + + // add the last turn + group.monsters.keySet().stream() + .filter(i -> !this.monsterOrders.contains(i)) + .forEach(this.monsterOrders::add); + for (int i = 0; i < sceneLimit; i++) { + spawnMonstersInGroup(group, group.monsters.get(this.monsterOrders.poll())); + } + } + public void spawnMonstersInGroup(SceneGroup group, SceneMonster monster) { + if(monster == null){ + return; + } + if(this.monsterSceneLimit > 0){ + this.monsterTideCount.decrementAndGet(); + this.monsterAlive.incrementAndGet(); } - List<GameEntity> toAdd = new ArrayList<>(); - - for (SceneMonster monster : monsters) { - MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); - - if (data == null) { - continue; - } - - // Calculate level - int level = monster.level; - - if (getScene().getDungeonData() != null) { - level = getScene().getDungeonData().getShowLevel(); - } else if (getScene().getWorld().getWorldLevel() > 0) { - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); - - if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); - } - } - - // Spawn mob - EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); - entity.getRotation().set(monster.rot); - entity.setGroupId(group.id); - entity.setConfigId(monster.config_id); - - toAdd.add(entity); + MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); + + if (data == null) { + return; } - - if (toAdd.size() > 0) { - getScene().addEntities(toAdd); - - for (GameEntity entity : toAdd) { - callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); + + // Calculate level + int level = monster.level; + + if (getScene().getDungeonData() != null) { + level = getScene().getDungeonData().getShowLevel(); + } else if (getScene().getWorld().getWorldLevel() > 0) { + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); + + if (worldLevelData != null) { + level = worldLevelData.getMonsterLevel(); + } + } + + // Spawn mob + EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); + entity.getRotation().set(monster.rot); + entity.setGroupId(group.id); + entity.setConfigId(monster.config_id); + + getScene().addEntity(entity); + + callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); + } + + public void onMonsterDie(){ + if(this.monsterSceneLimit <= 0){ + return; + } + if(this.monsterAlive.decrementAndGet() >= this.monsterSceneLimit) { + // maybe not happen + return; + } + if(this.monsterTideCount.get() > 0){ + // add more + spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); + }else if(this.monsterAlive.get() == 0){ + // spawn the last turn of monsters + while(!this.monsterOrders.isEmpty()){ + spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); } } } - // Events public void callEvent(int eventType, ScriptArgs params) { @@ -405,4 +434,8 @@ public class SceneScriptManager { } } } + +// public LuaValue safetyCall(){ +// +// } } diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 941b00b60..1b9badc11 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -1,28 +1,24 @@ package emu.grasscutter.scripts; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.game.dungeons.DungeonChallenge; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityMonster; import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.SceneRegion; -import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketGadgetStateNotify; import emu.grasscutter.server.packet.send.PacketWorktopOptionNotify; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; public class ScriptLib { + public static final Logger logger = LoggerFactory.getLogger(ScriptLib.class); private final SceneScriptManager sceneScriptManager; public ScriptLib(SceneScriptManager sceneScriptManager) { @@ -34,6 +30,8 @@ public class ScriptLib { } public int SetGadgetStateByConfigId(int configId, int gadgetState) { + logger.debug("[LUA] Call SetGadgetStateByConfigId with {},{}", + configId,gadgetState); Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId).findFirst(); @@ -53,6 +51,8 @@ public class ScriptLib { } public int SetGroupGadgetStateByConfigId(int groupId, int configId, int gadgetState) { + logger.debug("[LUA] Call SetGroupGadgetStateByConfigId with {},{},{}", + groupId,configId,gadgetState); List<GameEntity> list = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getGroupId() == groupId).toList(); @@ -71,6 +71,8 @@ public class ScriptLib { } public int SetWorktopOptionsByGroupId(int groupId, int configId, int[] options) { + logger.debug("[LUA] Call SetWorktopOptionsByGroupId with {},{},{}", + groupId,configId,options); Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId).findFirst(); @@ -90,6 +92,8 @@ public class ScriptLib { } public int DelWorktopOptionByGroupId(int groupId, int configId, int option) { + logger.debug("[LUA] Call DelWorktopOptionByGroupId with {},{},{}",groupId,configId,option); + Optional<GameEntity> entity = getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId).findFirst(); @@ -109,20 +113,24 @@ public class ScriptLib { } // Some fields are guessed - public int AutoMonsterTide(int challengeIndex, int groupId, int[] config_ids, int param4, int param5, int param6) { + public int AutoMonsterTide(int challengeIndex, int groupId, Integer[] ordersConfigId, int tideCount, int sceneLimit, int param6) { + logger.debug("[LUA] Call AutoMonsterTide with {},{},{},{},{},{}", + challengeIndex,groupId,ordersConfigId,tideCount,sceneLimit,param6); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); if (group == null || group.monsters == null) { return 1; } - - // TODO just spawn all from group for now - this.getSceneScriptManager().spawnMonstersInGroup(group); + + this.getSceneScriptManager().spawnMonstersInGroup(group, ordersConfigId, tideCount, sceneLimit); return 0; } public int AddExtraGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call AddExtraGroupSuite with {},{}", + groupId,suite); SceneGroup group = getSceneScriptManager().getGroupById(groupId); if (group == null || group.monsters == null) { @@ -136,8 +144,17 @@ public class ScriptLib { } // param3 (probably time limit for timed dungeons) - public int ActiveChallenge(int challengeId, int challengeIndex, int param3, int groupId, int objectiveKills, int param5) { + public int ActiveChallenge(int challengeId, int challengeIndex, int timeLimitOrGroupId, int groupId, int objectiveKills, int param5) { + logger.debug("[LUA] Call ActiveChallenge with {},{},{},{},{},{}", + challengeId,challengeIndex,timeLimitOrGroupId,groupId,objectiveKills,param5); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + var objective = objectiveKills; + + if(group == null){ + group = getSceneScriptManager().getGroupById(timeLimitOrGroupId); + objective = groupId; + } if (group == null || group.monsters == null) { return 1; @@ -146,7 +163,7 @@ public class ScriptLib { DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), group); challenge.setChallengeId(challengeId); challenge.setChallengeIndex(challengeIndex); - challenge.setObjective(objectiveKills); + challenge.setObjective(objective); getSceneScriptManager().getScene().setChallenge(challenge); @@ -155,26 +172,37 @@ public class ScriptLib { } public int GetGroupMonsterCountByGroupId(int groupId) { + logger.debug("[LUA] Call GetGroupMonsterCountByGroupId with {}", + groupId); return (int) getSceneScriptManager().getScene().getEntities().values().stream() .filter(e -> e instanceof EntityMonster && e.getGroupId() == groupId) .count(); } public int GetGroupVariableValue(String var) { + logger.debug("[LUA] Call GetGroupVariableValue with {}", + var); return getSceneScriptManager().getVariables().getOrDefault(var, 0); } public int SetGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call SetGroupVariableValue with {},{}", + var, value); getSceneScriptManager().getVariables().put(var, value); return 0; } public LuaValue ChangeGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call ChangeGroupVariableValue with {},{}", + var, value); + getSceneScriptManager().getVariables().put(var, getSceneScriptManager().getVariables().get(var) + value); return LuaValue.ZERO; } public int RefreshGroup(LuaTable table) { + logger.debug("[LUA] Call RefreshGroup with {}", + table); // Kill and Respawn? int groupId = table.get("group_id").toint(); int suite = table.get("suite").toint(); @@ -192,6 +220,8 @@ public class ScriptLib { } public int GetRegionEntityCount(LuaTable table) { + logger.debug("[LUA] Call GetRegionEntityCount with {}", + table); int regionId = table.get("region_eid").toint(); int entityType = table.get("entity_type").toint(); @@ -205,21 +235,68 @@ public class ScriptLib { } public void PrintContextLog(String msg) { - Grasscutter.getLogger().info("[LUA] " + msg); + logger.info("[LUA] " + msg); } - public int TowerCountTimeStatus(int var1, int var2){ + public int TowerCountTimeStatus(int isDone, int var2){ + logger.debug("[LUA] Call TowerCountTimeStatus with {},{}", + isDone,var2); + // TODO record time return 0; } public int GetGroupMonsterCount(int var1){ - // Maybe... - return GetGroupMonsterCountByGroupId(var1); + logger.debug("[LUA] Call GetGroupMonsterCount with {}", + var1); + + return (int) getSceneScriptManager().getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster) + .count(); } public int SetMonsterBattleByGroup(int var1, int var2, int var3){ + logger.debug("[LUA] Call SetMonsterBattleByGroup with {},{},{}", + var1,var2,var3); + return 0; } public int CauseDungeonFail(int var1){ + logger.debug("[LUA] Call CauseDungeonFail with {}", + var1); + return 0; } + // 8-1 + public int GetGroupVariableValueByGroup(int var1, String var2, int var3){ + logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{},{}", + var1,var2,var3); + + //TODO + + return getSceneScriptManager().getVariables().getOrDefault(var2, 0); + } + + public int SetIsAllowUseSkill(int canUse, int var2){ + logger.debug("[LUA] Call SetIsAllowUseSkill with {},{}", + canUse,var2); + + getSceneScriptManager().getScene().broadcastPacket(new PacketCanUseSkillNotify(canUse == 1)); + return 0; + } + + public int KillEntityByConfigId(LuaTable table){ + logger.debug("[LUA] Call KillEntityByConfigId with {}", + table); + var configId = table.get("config_id"); + if(configId == LuaValue.NIL){ + return 1; + } + + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId.toint()); + if(entity == null){ + return 1; + } + getSceneScriptManager().getScene().killEntity(entity, 0); + return 0; + } + } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index a13db7b68..690cd3d0d 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -1,17 +1,21 @@ package emu.grasscutter.scripts.data; -import java.util.List; - import emu.grasscutter.utils.Position; +import java.util.List; +import java.util.Map; + public class SceneGroup { public transient int block_id; // Not an actual variable in the scripts but we will keep it here for reference public int id; public int refresh_id; public Position pos; - - public List<SceneMonster> monsters; + + /** + * ConfigId - Monster + */ + public Map<Integer,SceneMonster> monsters; public List<SceneGadget> gadgets; public List<SceneTrigger> triggers; public List<SceneRegion> regions; diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java new file mode 100644 index 000000000..f8fe1314a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCanUseSkillNotify.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CanUseSkillNotifyOuterClass; + +public class PacketCanUseSkillNotify extends BasePacket { + + public PacketCanUseSkillNotify(boolean canUseSkill) { + super(PacketOpcodes.CanUseSkillNotify); + + CanUseSkillNotifyOuterClass.CanUseSkillNotify proto = CanUseSkillNotifyOuterClass.CanUseSkillNotify.newBuilder() + .setIsCanUseSkill(canUseSkill) + .build(); + + this.setData(proto); + } + +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 91d3f133c..1fc6831cb 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -19,4 +19,6 @@ <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> + <logger name="emu.grasscutter.scripts.ScriptLib" level="DEBUG"> + </logger> </Configuration> \ No newline at end of file From a269ff9563d5fa4040016730239c80aa7befa0f7 Mon Sep 17 00:00:00 2001 From: Zakhil <piotr.blecharski02@gmail.com> Date: Sun, 8 May 2022 03:19:24 +0200 Subject: [PATCH 186/434] Added polish locale (#655) --- src/main/resources/languages/pl-PL.json | 298 ++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/main/resources/languages/pl-PL.json diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json new file mode 100644 index 000000000..e9ff74e25 --- /dev/null +++ b/src/main/resources/languages/pl-PL.json @@ -0,0 +1,298 @@ +{ + "messages": { + "game": { + "port_bind": "Serwer gry uruchomiony na porcie: %s", + "connect": "Klient połączył się z %s", + "disconnect": "Klient rozłączył się z %s", + "game_update_error": "Wystąpił błąd podczas aktualizacji gry.", + "command_error": "Błąd komendy:" + }, + "dispatch": { + "port_bind": "[Dispatch] Serwer dispatch wystartował na porcie %s", + "request": "[Dispatch] Klient %s %s zapytanie: %s", + "keystore": { + "general_error": "[Dispatch] Błąd łądowania keystore!", + "password_error": "[Dispatch] Nie można załadować keystore. Próba z domyślnym hasłem keystore...", + "no_keystore_error": "[Dispatch] Brak certyfikatu SSL! Przejście na serwer HTTP.", + "default_password": "[Dispatch] Domyślne hasło keystore zadziałało. Rozważ ustawienie go na 123456 w pliku config.json." + }, + "no_commands_error": "Komendy nie są wspierane w trybie DISPATCH_ONLY.", + "unhandled_request_error": "[Dispatch] Potencjalnie niepodtrzymane %s zapytanie: %s", + "account": { + "login_attempt": "[Dispatch] Klient %s próbuje się zalogować", + "login_success": "[Dispatch] Klient %s zalogował się jako %s", + "login_token_attempt": "[Dispatch] Klient %s próbuje się zalogować poprzez token", + "login_token_error": "[Dispatch] Klient %s nie mógł się zalogować poprzez token", + "login_token_success": "[Dispatch] Klient %s zalogował się poprzez token jako %s", + "combo_token_success": "[Dispatch] Klient %s pomyślnie wymienił combo token", + "combo_token_error": "[Dispatch] Klient %s nie wymienił combo token'u", + "account_login_create_success": "[Dispatch] Klient %s nie mógł się zalogować: Konto %s stworzone", + "account_login_create_error": "[Dispatch] Klient %s nie mógł się zalogować: Tworzenie konta nie powiodło się", + "account_login_exist_error": "[Dispatch] Klient %s nie mógł się zalogować: Nie znaleziono konta", + "account_cache_error": "Błąd pamięci cache konta gry", + "session_key_error": "Błędny klucz sesji.", + "username_error": "Nazwa użytkownika nie znaleziona.", + "username_create_error": "Nazwa użytkownika nie znaleziona, tworzenie nie powiodło się." + } + }, + "status": { + "free_software": "Grasscutter to DARMOWE oprogramowanie. Jeżeli ktoś Ci je sprzedał, to zostałeś oscamowany. Strona domowa: https://github.com/Grasscutters/Grasscutter", + "starting": "Uruchamianie Grasscutter...", + "shutdown": "Wyłączanie...", + "done": "Gotowe! Wpisz \"help\" aby uzyskać pomoc", + "error": "Wystąpił błąd.", + "welcome": "Witamy w Grasscutter", + "run_mode_error": "Błędny tryb pracy serwera: %s.", + "run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...", + "create_resources": "Tworzenie folderu resources...", + "resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources." + } + }, + "commands": { + "generic": { + "not_specified": "Nie podano komendy.", + "unknown_command": "Nieznana komenda: %s", + "permission_error": "Nie masz uprawnień do tej komendy.", + "console_execute_error": "Tą komende można wywołać tylko z konsoli.", + "player_execute_error": "Wywołaj tą komendę w grze.", + "command_exist_error": "Nie znaleziono komendy.", + "invalid": { + "amount": "Błędna ilość.", + "artifactId": "Błędne ID artefaktu.", + "avatarId": "Błędne id postaci.", + "avatarLevel": "Błędny poziom postaci.", + "entityId": "Błędne id obiektu.", + "id przedmiotu": "Błędne id przedmiotu.", + "itemLevel": "Błędny poziom przedmiotu.", + "itemRefinement": "Błędne ulepszenie.", + "playerId": "Błędne playerId.", + "uid": "Błędne UID." + } + }, + "execution": { + "uid_error": "Błędne UID.", + "player_exist_error": "Gracz nie znaleziony.", + "player_offline_error": "Gracz nie jest online.", + "item_id_error": "Błędne ID przedmiotu.", + "item_player_exist_error": "Błędny przedmiot lub UID.", + "entity_id_error": "Błędne ID obiektu.", + "player_exist_offline_error": "Gracz nie znaleziony lub jest offline.", + "argument_error": "Błędne argumenty.", + "clear_target": "Cel wyczyszczony.", + "set_target": "Następne komendy będą celować w @%s.", + "need_target": "Ta komenda wymaga docelowego UID. Dodaj argument <@UID> lub ustaw stały cel poleceniem /target @UID." + }, + "status": { + "enabled": "Włączone", + "disabled": "Wyłączone", + "help": "Pomoc", + "success": "Sukces" + }, + "account": { + "modify": "Modyfikuj konta użytkowników", + "invalid": "Błędne UID.", + "exists": "Konto już istnieje.", + "create": "Stworzono konto z UID %s.", + "delete": "Konto usunięte.", + "no_account": "Nie znaleziono konta.", + "command_usage": "Użycie: account <create|delete> <nazwa> [uid]" + }, + "broadcast": { + "command_usage": "Użycie: broadcast <wiadomość>", + "message_sent": "Wiadomość wysłana." + }, + "changescene": { + "usage": "Użycie: changescene <id sceny>", + "already_in_scene": "Już jesteś na tej scenie.", + "success": "Zmieniono scene na %s.", + "exists_error": "Ta scena nie istenieje." + }, + "clear": { + "command_usage": "Użycie: clear <all|wp|art|mat>", + "weapons": "Wyczyszczono bronie dla %s.", + "artifacts": "Wyczyszczono artefakty dla %s.", + "materials": "Wyczyszczono materiały dla %s.", + "furniture": "Wyczyszczono meble dla %s.", + "displays": "Wyczyszczono displays dla %s.", + "virtuals": "Wyczyszczono virtuals dla %s.", + "everything": "Wyczyszczono wszystko dla %s." + }, + "coop": { + "usage": "Użycie: coop <id gracza> <id gracza docelowego>", + "success": "Przyzwano %s do świata %s." + }, + "enter_dungeon": { + "usage": "Użycie: enterdungeon <ID lochu>", + "changed": "Zmieniono loch na %s", + "not_found_error": "Ten loch nie istnieje", + "in_dungeon_error": "Już jesteś w tym lochu" + }, + "giveAll": { + "usage": "Użycie: giveall [gracz] [ilość]", + "started": "Dodawanie wszystkich przedmiotów...", + "success": "Pomyślnie dodano wszystkie przedmioty dla %s.", + "invalid_amount_or_playerId": "Błędna ilość lub ID gracza." + }, + "giveArtifact": { + "usage": "Użycie: giveart|gart [gracz] <id artefaktu> <mainPropId> [<appendPropId>[,<razy>]]... [poziom]", + "id_error": "Błędne ID artefaktu.", + "success": "Dano %s dla %s." + }, + "giveChar": { + "usage": "Użycie: givechar <gracz> <id przedmiotu | nazwa przedmiotu> [ilość]", + "given": "Dano %s z poziomem %s dla %s.", + "invalid_avatar_id": "Błędne ID postaci.", + "invalid_avatar_level": "Błędny poziom postaci.", + "invalid_avatar_or_player_id": "Błędne ID postaci lub gracza." + }, + "give": { + "usage": "Użycie: give <gracz> <id przedmiotu | nazwa przedmiotu> [ilość] [poziom]", + "refinement_only_applicable_weapons": "Ulepszenie można zastosować tylko dla broni.", + "refinement_must_between_1_and_5": "Ulepszenie musi być pomiędzy 1, a 5.", + "given": "Dano %s %s dla %s.", + "given_with_level_and_refinement": "Dano %s z poziomem %s, ulepszeniem %s %s razy dla %s", + "given_level": "Dano %s z poziomem %s %s razy dla %s" + }, + "godmode": { + "success": "Godmode jest teraz %s dla %s." + }, + "heal": { + "success": "Wszystkie postacie zostały wyleczone." + }, + "kick": { + "player_kick_player": "Gracz [%s:%s] wyrzucił gracza [%s:%s]", + "server_kick_player": "Wyrzucono gracza [%s:%s]" + }, + "kill": { + "usage": "Użycie: killall [UID gracza] [ID sceny]", + "scene_not_found_in_player_world": "Scena nie znaleziona w świecie gracza", + "kill_monsters_in_scene": "Zabito %s potworów w scenie %s" + }, + "killCharacter": { + "usage": "Użycie: /killcharacter [ID gracza]", + "success": "Zabito aktualną postać gracza %s." + }, + "list": { + "success": "Teraz jest %s gracz(y) online:" + }, + "permission": { + "usage": "Użycie: permission <add|remove> <nazwa gracza> <uprawnienie>", + "add": "Dodano uprawnienie", + "has_error": "To konto już ma to uprawnienie!", + "remove": "Usunięto uprawnienie.", + "not_have_error": "To konto nie ma tych uprawnień!", + "account_error": "Konto nie może zostać znalezione." + }, + "position": { + "success": "Koordynaty: %.3f, %.3f, %.3f\nID sceny: %d" + }, + "reload": { + "reload_start": "Ponowne ładowanie konfiguracji.", + "reload_done": "Ponowne ładowanie zakończone." + }, + "resetConst": { + "reset_all": "Resetuj konstelacje wszystkich postaci.", + "success": "Konstelacje dla %s zostały zresetowane. Proszę zalogować się ponownie aby zobaczyć zmiany." + }, + "resetShopLimit": { + "usage": "Użycie: /resetshop <ID gracza>" + }, + "sendMail": { + "usage": "Użycie: `/sendmail <ID gracza | all | help> [id szablonu]`", + "user_not_exist": "Gracz o ID '%s' nie istnieje", + "start_composition": "Komponowanie wiadomości.\nProszę użyj `/sendmail <tytuł>` aby kontynuować.\nMożesz użyć `/sendmail stop` w dowolnym momencie", + "templates": "Szablony zostaną zaimplementowane niedługo...", + "invalid_arguments": "Błędne argumenty.\nUżycie `/sendmail <ID gracza | all | help> [id szablonu]`", + "send_cancel": "Anulowano wysyłanie wiadomości", + "send_done": "Wysłano wiadomość do gracza %s!", + "send_all_done": "Wysłano wiadomośc do wszystkich graczy!", + "not_composition_end": "Komponowanie nie jest na ostatnim etapie.\nProszę użyj `/sendmail %s` lub `/sendmail stop` aby anulować", + "please_use": "Proszę użyj `/sendmail %s`", + "set_title": "Tytuł wiadomości to teraz: '%s'.\nUżyj '/sendmail <treść>' aby kontynuować.", + "set_contents": "Treść wiadomości to teraz '%s'.\nUżyj '/sendmail <nadawca>' aby kontynuować.", + "set_message_sender": "Nadawca wiadomości to teraz '%s'.\nUżyj '/sendmail <id przedmiotu | nazwa przedmiotu | zakończ> [ilość] [poziom]' aby kontynuować.", + "send": "Załączono %s %s (poziom %s) do wiadomości.\nDodaj więcej przedmiotów lub użyj `/sendmail finish` aby wysłać wiadomość.", + "invalid_arguments_please_use": "Błędne argumenty \nProszę użyj `/sendmail %s`", + "title": "<tytuł>", + "message": "<wiadomość>", + "sender": "<nadawca>", + "arguments": "<id przedmiotu | nazwa przedmiotu | zakończ> [ilość] [poziom]", + "error": "BŁĄD: niepoprawny etap konstrukcji: %s. Sprawdź konsolę aby dowiedzieć się więcej." + }, + "sendMessage": { + "usage": "Użycie: /sendmessage <player> <message>", + "success": "Wiadomość wysłana." + }, + "setFetterLevel": { + "usage": "Użycie: setfetterlevel <poziom>", + "range_error": "Poziom przyjaźni musi być pomiędzy 0,a 10.", + "success": "Poziom przyjaźni ustawiono na: %s", + "level_error": "Błędny poziom przyjaźni." + }, + "setStats": { + "usage_console": "Użycie: setstats|stats @<UID> <statystyka> <wartość>", + "usage_ingame": "Użycie: setstats|stats [@UID] <statystyka> <wartość>", + "help_message": "\n\tWartości dla Statystyka: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus DMG żywiołu: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) RES na żywioł: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n", + "value_error": "Błędna wartość statystyki.", + "uid_error": "Błędne UID.", + "player_error": "Gracza nie znaleziono lub jest offline.", + "set_self": "%s ustawiono na %s.", + "set_for_uid": "%s dla %s ustawiono na %s.", + "set_max_hp": "Maksymalne HP ustawione na %s." + }, + "setWorldLevel": { + "usage": "Użycie: setworldlevel <poziom>", + "value_error": "Poziom świata musi być pomiędzy 0, a 8", + "success": "Ustawiono poziom świata na: %s.", + "invalid_world_level": "Invalid world level." + }, + "spawn": { + "usage": "Użycie: /spawn <id obiektu> [ilość] [poziom(tylko potwory)]", + "success": "Stworzono %s %s." + }, + "stop": { + "success": "Serwer wyłącza się..." + }, + "talent": { + "usage_1": "Aby ustawić poziom talentu: /talent set <ID talentu> <wartość>", + "usage_2": "Inny sposób na ustawienie poziomu talentu: /talent <n lub e lub q> <wartość>", + "usage_3": "Aby uzyskać ID talentu: /talent getid", + "lower_16": "Błędny poziom talentu. Poziom powinien być mniejszy niż 16", + "set_id": "Ustawiono talent na %s.", + "set_atk": "Ustawiono talent Atak Podstawowy na poziom %s.", + "set_e": "Ustawiono poziom talentu E na %s.", + "set_q": "Ustawiono poziom talentu Q na %s.", + "invalid_skill_id": "Błędne ID umiejętności.", + "set_this": "Ustawiono ten talent na poziom %s.", + "invalid_level": "Błędny poziom talentu.", + "normal_attack_id": "ID podstawowego ataku: %s.", + "e_skill_id": "ID umiejętności E: %s.", + "q_skill_id": "ID umiejętności Q: %s." + }, + "teleportAll": { + "success": "Przyzwano wszystkich graczy do Ciebie.", + "error": "Możesz użyć tej komendy wyłącznie w trybie MP." + }, + "teleport": { + "usage_server": "Użycie: /tp @<ID gracza> <x> <y> <z> [ID sceny]", + "usage": "Użycie: /tp [@<ID gracza>] <x> <y> <z> [ID sceny]", + "specify_player_id": "Musisz określić ID gracza.", + "invalid_position": "Błędna pozycja.", + "success": "Przeteleportowano %s do %s, %s, %s w scenie %s" + }, + "weather": { + "usage": "Użycie: weather <ID pogody> [ID klimatu]", + "success": "Zmieniono pogodę na %s z klimatem %s", + "invalid_id": "Błędne ID." + }, + "drop": { + "command_usage": "Użycie: drop <ID przedmiotu|nazwa przedmiotu> [ilość]", + "success": "Wyrzucono %s of %s." + }, + "help": { + "usage": "Użycie: ", + "aliases": "Aliasy: ", + "available_commands": "Dostępne komendy: " + } + } +} \ No newline at end of file From 2416dd66e59f53d3bdc641a2649fac9b59019a9e Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Sat, 7 May 2022 23:44:35 -0400 Subject: [PATCH 187/434] Fix language fallback'ing --- src/main/java/emu/grasscutter/utils/Language.java | 6 ++++-- src/main/java/emu/grasscutter/utils/Utils.java | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 0af77adc1..cda46e512 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -48,11 +48,13 @@ public final class Language { try { InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); - if(file == null) { + String translationContents = Utils.readFromInputStream(file); + if(translationContents.equals("empty")) { file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); + translationContents = Utils.readFromInputStream(file); } - languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); + languageData = Grasscutter.getGsonFactory().fromJson(translationContents, JsonObject.class); } catch (Exception exception) { Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception); } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 6d11822f0..1d79c496e 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -18,6 +18,8 @@ import io.netty.buffer.Unpooled; import org.slf4j.Logger; +import javax.annotation.Nullable; + import static emu.grasscutter.utils.Language.translate; @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) @@ -253,7 +255,9 @@ public final class Utils { * @param stream The input stream. * @return The string. */ - public static String readFromInputStream(InputStream stream) { + public static String readFromInputStream(@Nullable InputStream stream) { + if(stream == null) return "empty"; + StringBuilder stringBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { @@ -261,6 +265,8 @@ public final class Utils { } stream.close(); } catch (IOException e) { Grasscutter.getLogger().warn("Failed to read from input stream."); + } catch (NullPointerException ignored) { + return "empty"; } return stringBuilder.toString(); } From e99875ea4dee2c914822011b5888f296549e46c6 Mon Sep 17 00:00:00 2001 From: Magix <27646710+KingRainbow44@users.noreply.github.com> Date: Sat, 7 May 2022 21:24:18 -0400 Subject: [PATCH 188/434] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49a9d8b53..d14ebf3e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,4 +31,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: Grasscutter - path: grasscutter-*-dev.jar + path: grasscutter-*.jar From 04a58d43d56a314b56dc341015179635a8267e8b Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Sat, 7 May 2022 19:39:31 -0400 Subject: [PATCH 189/434] Add a plugin schema --- plugin-schema.json | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 plugin-schema.json diff --git a/plugin-schema.json b/plugin-schema.json new file mode 100644 index 000000000..4fc772416 --- /dev/null +++ b/plugin-schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "JSON schema for a Grasscutter Plugin", + "type": "object", + "additionalProperties": true, + "definitions": { + "plugin-name": { + "type": "string", + "pattern": "^[A-Za-z\\d_.-]+$" + } + }, + "required": [ "name", "description", "mainClass" ], + "properties": { + "name": { + "description": "The unique name of plugin.", + "$ref": "#/definitions/plugin-name" + }, + "mainClass": { + "description": "The plugin's initial class file.", + "type": "string", + "pattern": "^(?!org\\.bukkit\\.)([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*$" + }, + "version": { + "description": "A plugin revision identifier.", + "type": [ "string", "number" ] + }, + "description": { + "description": "Human readable plugin summary.", + "type": "string" + }, + "author": { + "description": "The plugin author.", + "type": "string" + }, + "authors": { + "description": "The plugin contributors.", + "type": "array", + "items": { + "type": "string" + } + }, + "website": { + "title": "Website", + "description": "The URL to the plugin's site", + "type": "string", + "format": "uri" + } + } +} \ No newline at end of file From 8ddd7b125dd28a9551398c520ffc06f534bf955c Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sat, 7 May 2022 21:07:06 -0700 Subject: [PATCH 190/434] Fix null reference on stamina timer when paused player disconnects. --- .../StaminaManager/StaminaManager.java | 30 ++++++++----------- .../emu/grasscutter/game/player/Player.java | 4 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 3d55e5edf..5065b12b3 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -29,19 +29,16 @@ public class StaminaManager { private Position previousCoordinates = new Position(0, 0, 0); private MotionState currentState = MotionState.MOTION_STANDBY; private MotionState previousState = MotionState.MOTION_STANDBY; - private Timer sustainedStaminaHandlerTimer; + private final Timer sustainedStaminaHandlerTimer = new Timer(); + private final SustainedStaminaHandler handleSustainedStamina = new SustainedStaminaHandler(); + private boolean timerRunning = false; private GameSession cachedSession = null; private GameEntity cachedEntity = null; private int staminaRecoverDelay = 0; private boolean isInSkillMove = false; - - - - public boolean getIsInSkillMove() { return isInSkillMove; } - public void setIsInSkillMove(boolean b) { isInSkillMove = b; } @@ -139,24 +136,23 @@ public class StaminaManager { entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); player.getScene().removeEntity(entity); - ((EntityAvatar) entity).onDeath(dieType, 0); + ((EntityAvatar)entity).onDeath(dieType, 0); } public void startSustainedStaminaHandler() { - if (!player.isPaused()) { - if (sustainedStaminaHandlerTimer == null) { - sustainedStaminaHandlerTimer = new Timer(); - sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); - Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); - } + if (!player.isPaused() && !timerRunning) { + timerRunning = true; + sustainedStaminaHandlerTimer.scheduleAtFixedRate(handleSustainedStamina, 0, 200); + // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); } - } public void stopSustainedStaminaHandler() { - Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); - sustainedStaminaHandlerTimer.cancel(); - sustainedStaminaHandlerTimer = null; + if (timerRunning) { + timerRunning = false; + sustainedStaminaHandlerTimer.cancel(); + // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); + } } // Handlers diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index b6956153c..71ae9d8c6 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -872,11 +872,11 @@ public class Player { } public void onPause() { - staminaManager.stopSustainedStaminaHandler(); + getStaminaManager().stopSustainedStaminaHandler(); } public void onUnpause() { - staminaManager.startSustainedStaminaHandler(); + getStaminaManager().startSustainedStaminaHandler(); } public void sendPacket(BasePacket packet) { From 8b198d6dbeaa095a1653031351633a4bef805003 Mon Sep 17 00:00:00 2001 From: Mateoust <46558043+Mateoust@users.noreply.github.com> Date: Sun, 8 May 2022 13:03:14 +0800 Subject: [PATCH 191/434] fix issues 646 --- src/main/resources/languages/zh-CN.json | 2 +- src/main/resources/languages/zh-TW.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index b9bc5b22a..4ce6196bc 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -173,7 +173,7 @@ "success": "已杀死 %s 目前使用的角色。" }, "list": { - "message": "目前在线人数:%s" + "success": "目前在线人数:%s" }, "permission": { "usage": "用法:permission <add|remove> <username> <permission>", diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 49e0f256d..7a7f5c100 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -173,7 +173,7 @@ "success": "已殺死 %s 目前的場上角色。" }, "list": { - "message": "目前總線上人數:%s" + "success": "目前總線上人數:%s" }, "permission": { "usage": "用法:permission <add|remove> <username> <permission>", From 401cb609ae543610e608290ed91cab9eb6d60460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=8A=E6=9E=AB?= <i@foxex.cn> Date: Sun, 8 May 2022 10:45:16 +0800 Subject: [PATCH 192/434] Fix typo && update zh-CN.json --- src/main/resources/languages/zh-CN.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 4ce6196bc..e78b76776 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -36,10 +36,10 @@ } }, "status": { - "free_software": "Grasscutter 是免费开源软件。如果你是付费购买的,那已经被骗了。Github:https://github.com/Grasscutters/Grasscutter", + "free_software": "Grasscutter 是免费开源软件,遵循Apache-2.0 license。如果您是付费购买的,那您已经被骗了。项目地址:Github:https://github.com/Grasscutters/Grasscutter", "starting": "正在启动 Grasscutter...", "shutdown": "正在关闭...", - "done": "加載完成!输入 \"help\" 查看命令列表", + "done": "加载完成!输入 \"help\" 查看命令列表", "error": "发生了一个错误。", "welcome": "欢迎使用 Grasscutter", "run_mode_error": "无效的服务器运行模式: %s。", @@ -119,7 +119,7 @@ }, "coop": { "usage": "用法:coop <playerId> <target playerId>", - "success": "已召唤 %s 到 %s的世界" + "success": "已强制召唤 %s 到 %s的世界" }, "enter_dungeon": { "usage": "用法:enterdungeon <dungeon id>", @@ -188,7 +188,7 @@ }, "reload": { "reload_start": "正在重载配置文件和数据。", - "reload_done": "重装完毕。" + "reload_done": "重载完毕。" }, "resetConst": { "reset_all": "重置所有角色的命座。", @@ -260,14 +260,14 @@ "lower_16": "无效的天赋等级,天赋等级应低于16。", "set_id": "将天赋等级设为 %s。", "set_atk": "将普通攻击等级设为 %s。", - "set_e": "设定天赋E等级为 %s。", - "set_q": "设定天赋Q等级为 %s。", + "set_e": "设定元素战技等级为 %s。", + "set_q": "设定元素爆发等级为 %s。", "invalid_skill_id": "无效的技能ID。", "set_this": "将天赋等级设为 %s。", "invalid_level": "无效的天赋等级。", "normal_attack_id": "普通攻击的 ID 为 %s。", - "e_skill_id": "E技能ID %s。", - "q_skill_id": "Q技能ID %s。" + "e_skill_id": "元素战技ID %s。", + "q_skill_id": "元素爆发ID %s。" }, "teleportAll": { "success": "已将全部玩家传送到你的位置", @@ -287,7 +287,7 @@ }, "drop": { "command_usage": "用法:drop <itemId|itemName> [amount]", - "success": "已將 %s x %s 丟在附近。" + "success": "已将 %s x %s 丟在附近。" }, "help": { "usage": "用法:", From 57236f2197601758515a7b214072a1c3c0628cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=8A=E6=9E=AB?= <i@foxex.cn> Date: Sun, 8 May 2022 11:38:56 +0800 Subject: [PATCH 193/434] Update zh-CN.json --- src/main/resources/languages/zh-CN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index e78b76776..18ff2165c 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -140,7 +140,7 @@ }, "giveChar": { "usage": "用法:givechar <player> <itemId|itemName> [amount]", - "given": "Given %s with level %s to %s.", + "given": "给予角色 %s 等级 %s 向UID %s.", "invalid_avatar_id": "无效的角色ID。", "invalid_avatar_level": "无效的角色等級。.", "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。" @@ -151,7 +151,7 @@ "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", "given": "已将 %s 个 %s 给予 %s。", "given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s", - "given_level": "已将 %s 等级 %s %s 个给予 %s" + "given_level": "已将 %s 等级 %s %s 个给予UID %s" }, "godmode": { "success": "上帝模式被设置为 %s 。 [用户:%s]" From 3dcf8bf46a7c5711e3923cdeed71fe942cb12066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=8A=E6=9E=AB?= <i@foxex.cn> Date: Sun, 8 May 2022 11:51:07 +0800 Subject: [PATCH 194/434] Update zh-CN.json --- src/main/resources/languages/zh-CN.json | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 18ff2165c..8faa0e4ae 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -41,7 +41,7 @@ "shutdown": "正在关闭...", "done": "加载完成!输入 \"help\" 查看命令列表", "error": "发生了一个错误。", - "welcome": "欢迎使用 Grasscutter", + "welcome": "欢迎使用 Grasscutter!珍惜这段美妙的旅途吧!", "run_mode_error": "无效的服务器运行模式: %s。", "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "create_resources": "正在创建 resources 目录...", @@ -52,12 +52,12 @@ "generic": { "not_specified": "没有指定命令。", "unknown_command": "未知的命令:%s", - "permission_error": "您没有执行此命令的权限。", - "console_execute_error": "此命令只能在服务器控制台执行。", - "player_execute_error": "此命令只能在游戏内执行。", - "command_exist_error": "找不到命令。", + "permission_error": "哼哼哼!您没有执行此命令的权限!请联系服务器管理员解决!", + "console_execute_error": "此命令只能在服务器控制台执行呐~", + "player_execute_error": "此命令只能在游戏内执行哦~", + "command_exist_error": "这条命令……好像找不到呢?。", "invalid": { - "amount": "无效的 数量.", + "amount": "无效的数量.", "artifactId": "无效的圣遗物ID。", "avatarId": "无效的角色ID。", "avatarLevel": "无效的角色等級。", @@ -147,14 +147,14 @@ }, "give": { "usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]", - "refinement_only_applicable_weapons": "精炼等级参数仅在武器上可用", - "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", + "refinement_only_applicable_weapons": "精炼等阶参数仅在给予武器时可用", + "refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。", "given": "已将 %s 个 %s 给予 %s。", "given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s", "given_level": "已将 %s 等级 %s %s 个给予UID %s" }, "godmode": { - "success": "上帝模式被设置为 %s 。 [用户:%s]" + "success": "上帝模式已被设置为 %s 。 [用户:%s]" }, "heal": { "success": "所有角色已被治疗。" @@ -204,7 +204,7 @@ "templates": "邮件模板尚未实装...", "invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`", "send_cancel": "取消发送邮件", - "send_done": "已将邮件给 %s!", + "send_done": "已将邮件发送给 %s!", "send_all_done": "邮件已发送给所有人!", "not_composition_end": "现在邮件发送未到最后阶段。\n请使用 `/sendmail %s` 继续发送邮件,或使用 `/sendmail stop` 来停止发送邮件。", "please_use": "请使用 `/sendmail %s`", @@ -257,7 +257,7 @@ "usage_1": "设置天赋等级:/talent set <talentID> <value>", "usage_2": "另一种设置天赋等级的命令使用方法:/talent <n or e or q> <value>", "usage_3": "获取天赋ID指令用法:/talent getid", - "lower_16": "无效的天赋等级,天赋等级应低于16。", + "lower_16": "无效的天赋等级,天赋等级应小于等于15。", "set_id": "将天赋等级设为 %s。", "set_atk": "将普通攻击等级设为 %s。", "set_e": "设定元素战技等级为 %s。", @@ -271,7 +271,7 @@ }, "teleportAll": { "success": "已将全部玩家传送到你的位置", - "error": "命令仅限多人游戏使用。" + "error": "命令仅限处于多人游戏状态下使用。" }, "teleport": { "usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]", @@ -282,8 +282,8 @@ }, "weather": { "usage": "用法:weather <weatherId> [climateId]", - "success": "已将当前天气设定为 %s,气候则为 %s。", - "invalid_id": "无效的ID。" + "success": "已将当前天气设定为 %s,气候为 %s。", + "invalid_id": "无效的天气ID。" }, "drop": { "command_usage": "用法:drop <itemId|itemName> [amount]", From b69c58cab1b26fbc695a9f48946748c69c3f1330 Mon Sep 17 00:00:00 2001 From: Magix <27646710+KingRainbow44@users.noreply.github.com> Date: Sat, 7 May 2022 21:24:18 -0400 Subject: [PATCH 195/434] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49a9d8b53..d14ebf3e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,4 +31,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: Grasscutter - path: grasscutter-*-dev.jar + path: grasscutter-*.jar From 05e1e5502c33c97e2f92c7651c745e0260bda519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= <fkj@fkj233.cn> Date: Sun, 8 May 2022 10:33:53 +0800 Subject: [PATCH 196/434] Add command description multilingual --- .../java/emu/grasscutter/command/Command.java | 2 - .../grasscutter/command/CommandHandler.java | 3 + .../emu/grasscutter/command/CommandMap.java | 8 ++ .../command/commands/AccountCommand.java | 7 +- .../command/commands/BroadcastCommand.java | 8 +- .../command/commands/ChangeSceneCommand.java | 8 +- .../command/commands/ClearCommand.java | 6 +- .../command/commands/CoopCommand.java | 8 +- .../command/commands/DropCommand.java | 8 +- .../command/commands/EnterDungeonCommand.java | 8 +- .../command/commands/GiveAllCommand.java | 8 +- .../command/commands/GiveArtifactCommand.java | 7 +- .../command/commands/GiveCharCommand.java | 8 +- .../command/commands/GiveCommand.java | 10 +- .../command/commands/GodModeCommand.java | 9 +- .../command/commands/HealCommand.java | 8 +- .../command/commands/HelpCommand.java | 24 ++-- .../command/commands/KickCommand.java | 8 +- .../command/commands/KillAllCommand.java | 8 +- .../commands/KillCharacterCommand.java | 8 +- .../command/commands/ListCommand.java | 8 +- .../command/commands/PermissionCommand.java | 8 +- .../command/commands/PositionCommand.java | 8 +- .../command/commands/ReloadCommand.java | 8 +- .../command/commands/ResetConstCommand.java | 6 +- .../commands/ResetShopLimitCommand.java | 8 +- .../command/commands/RestartCommand.java | 9 +- .../command/commands/SendMailCommand.java | 10 +- .../command/commands/SendMessageCommand.java | 7 +- .../commands/SetFetterLevelCommand.java | 6 +- .../command/commands/SetStatsCommand.java | 8 +- .../commands/SetWorldLevelCommand.java | 6 +- .../command/commands/SpawnCommand.java | 8 +- .../command/commands/StopCommand.java | 8 +- .../command/commands/TalentCommand.java | 8 +- .../command/commands/TeleportAllCommand.java | 8 +- .../command/commands/TeleportCommand.java | 8 +- .../command/commands/WeatherCommand.java | 8 +- .../java/emu/grasscutter/tools/Tools.java | 12 +- src/main/resources/languages/en-US.json | 105 ++++++++++++------ 40 files changed, 306 insertions(+), 115 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/Command.java b/src/main/java/emu/grasscutter/command/Command.java index 734f454ea..c94804678 100644 --- a/src/main/java/emu/grasscutter/command/Command.java +++ b/src/main/java/emu/grasscutter/command/Command.java @@ -9,8 +9,6 @@ public @interface Command { 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 index ffe21c9be..dadb87bb3 100644 --- a/src/main/java/emu/grasscutter/command/CommandHandler.java +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -6,6 +6,9 @@ import emu.grasscutter.game.player.Player; import java.util.List; public interface CommandHandler { + + String description(); + /** * Send a message to the target. * diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index a183c6ac3..6b6a852af 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -85,6 +85,14 @@ public final class CommandMap { return new LinkedHashMap<>(this.annotations); } + public HashMap<CommandHandler, Command> getHandlersAndAnnotations() { + HashMap<CommandHandler, Command> hashMap = new HashMap<>(); + this.commands.forEach((key, handler) -> { + hashMap.put(handler, this.annotations.get(key)); + }); + return hashMap; + } + /** * Returns a list of all registered commands. * diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 627f4680f..1c5c8228e 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -9,9 +9,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "account", usage = "account <create|delete> <username> [uid]", description = "Modify user accounts") +@Command(label = "account", usage = "account <create|delete> <username> [uid]") public final class AccountCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.account.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender != null) { diff --git a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java index 1aa234919..fcabb131c 100644 --- a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java @@ -9,10 +9,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "broadcast", usage = "broadcast <message>", - description = "Sends a message to all the players", aliases = {"b"}, permission = "server.broadcast") +@Command(label = "broadcast", usage = "broadcast <message>", aliases = {"b"}, permission = "server.broadcast") public final class BroadcastCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.broadcast.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (args.size() < 1) { diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index 1a4e97927..ea7bfe0f6 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -8,9 +8,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "changescene", usage = "changescene <scene id>", - description = "Changes your scene", aliases = {"scene"}, permission = "player.changescene") +@Command(label = "changescene", usage = "changescene <scene id>", aliases = {"scene"}, permission = "player.changescene") public final class ChangeSceneCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.changescene.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java index 47d9f2c0d..d416bec52 100644 --- a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java @@ -13,11 +13,15 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "clear", usage = "clear <all|wp|art|mat>", //Merged /clearartifacts and /clearweapons to /clear <args> [uid] - description = "Deletes unequipped unlocked items, including yellow rarity ones from your inventory", aliases = {"clear"}, permission = "player.clearinv") public final class ClearCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.clear.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index 96411019b..120665b76 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -9,9 +9,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "coop", usage = "coop [host UID]", - description = "Forces someone to join the world of others", permission = "server.coop") +@Command(label = "coop", usage = "coop [host UID]", permission = "server.coop") public final class CoopCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.coop.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java index a33a32603..a187cfda8 100644 --- a/src/main/java/emu/grasscutter/command/commands/DropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -13,10 +13,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", - description = "Drops an item near you", aliases = {"d", "dropitem"}, permission = "server.drop") +@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", aliases = {"d", "dropitem"}, permission = "server.drop") public final class DropCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.drop.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java index 434e80c8f..d92d537e9 100644 --- a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java @@ -8,9 +8,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", - description = "Enter a dungeon", aliases = {"dungeon"}, permission = "player.enterdungeon") +@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", aliases = {"dungeon"}, permission = "player.enterdungeon") public final class EnterDungeonCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.enter_dungeon.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index bb11de3c2..5009a4462 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -15,10 +15,14 @@ import java.util.*; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveall", usage = "giveall [amount]", - description = "Gives all items", aliases = {"givea"}, permission = "player.giveall", threading = true) +@Command(label = "giveall", usage = "giveall [amount]", aliases = {"givea"}, permission = "player.giveall", threading = true) public final class GiveAllCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.giveAll.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 541cc440e..d41e83671 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -16,8 +16,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", description = "Gives the player a specified artifact", aliases = {"gart"}, permission = "player.giveart") +@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart") public final class GiveArtifactCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.giveArtifact.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 4b3279202..0e784639d 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -12,10 +12,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "givechar", usage = "givechar <avatarId> [level]", - description = "Gives the player a specified character", aliases = {"givec"}, permission = "player.givechar") +@Command(label = "givechar", usage = "givechar <avatarId> [level]", aliases = {"givec"}, permission = "player.givechar") public final class GiveCharCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.giveChar.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index 2f020f7b3..3daa2dff3 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.data.GameData; @@ -12,12 +11,12 @@ import emu.grasscutter.game.props.ActionReason; import java.util.LinkedList; import java.util.List; -import java.util.regex.Pattern; import java.util.regex.Matcher; +import java.util.regex.Pattern; import static emu.grasscutter.utils.Language.translate; -@Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", description = "Gives an item to you or the specified player", aliases = { +@Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", aliases = { "g", "item", "giveitem"}, permission = "player.give") public final class GiveCommand implements CommandHandler { Pattern lvlRegex = Pattern.compile("l(?:vl?)?(\\d+)"); // Java is a joke of a proglang that doesn't have raw string literals @@ -32,6 +31,11 @@ public final class GiveCommand implements CommandHandler { return -1; } + @Override + public String description() { + return translate("commands.give.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java index 9abebb8db..4fd2999a6 100644 --- a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -1,6 +1,5 @@ package emu.grasscutter.command.commands; -import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; @@ -9,10 +8,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "godmode", usage = "godmode [on|off|toggle]", - description = "Prevents you from taking damage. Defaults to toggle.", permission = "player.godmode") +@Command(label = "godmode", usage = "godmode [on|off|toggle]", permission = "player.godmode") public final class GodModeCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.godmode.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java index bb0b861b0..b459ecb8c 100644 --- a/src/main/java/emu/grasscutter/command/commands/HealCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -11,9 +11,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "heal", usage = "heal|h", aliases = {"h"}, - description = "Heal all characters in your current team.", permission = "player.heal") +@Command(label = "heal", usage = "heal|h", aliases = {"h"}, permission = "player.heal") public final class HealCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.heal.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java index 93ac831b3..dbb85bf9d 100644 --- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -10,22 +10,26 @@ import java.util.*; import static emu.grasscutter.utils.Language.translate; -@Command(label = "help", usage = "help [command]", - description = "Sends the help message or shows information about a specified command") +@Command(label = "help", usage = "help [command]") public final class HelpCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.help.description"); + } + @Override public void execute(Player player, Player targetPlayer, List<String> args) { if (args.size() < 1) { HashMap<String, CommandHandler> handlers = CommandMap.getInstance().getHandlers(); - List<Command> annotations = new ArrayList<>(); + HashMap<Command, CommandHandler> annotations = new HashMap<>(); 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); + annotations.put(annotation, handlers.get(key)); } } @@ -39,7 +43,7 @@ public final class HelpCommand implements CommandHandler { } else { Command annotation = handler.getClass().getAnnotation(Command.class); - builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" ").append(handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -56,12 +60,12 @@ public final class HelpCommand implements CommandHandler { } } - void SendAllHelpMessage(Player player, List<Command> annotations) { + void SendAllHelpMessage(Player player, HashMap<Command, CommandHandler> annotations) { if (player == null) { StringBuilder builder = new StringBuilder("\n" + translate("commands.help.available_commands") + "\n"); - annotations.forEach(annotation -> { + annotations.forEach((annotation, handler) -> { builder.append(annotation.label()).append("\n"); - builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" ").append(handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -76,9 +80,9 @@ public final class HelpCommand implements CommandHandler { CommandHandler.sendMessage(null, builder.toString()); } else { CommandHandler.sendMessage(player, translate("commands.help.available_commands")); - annotations.forEach(annotation -> { + annotations.forEach((annotation, handler) -> { StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); - builder.append(" ").append(annotation.description()).append("\n"); + builder.append(" ").append(handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); diff --git a/src/main/java/emu/grasscutter/command/commands/KickCommand.java b/src/main/java/emu/grasscutter/command/commands/KickCommand.java index 270e28150..71b487cc4 100644 --- a/src/main/java/emu/grasscutter/command/commands/KickCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KickCommand.java @@ -8,10 +8,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "kick", usage = "kick", - description = "Kicks the specified player from the server (WIP)", permission = "server.kick") +@Command(label = "kick", usage = "kick", permission = "server.kick") public final class KickCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.kick.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java index 423c60bbd..1fa51eec4 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -12,10 +12,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killall", usage = "killall [sceneId]", - description = "Kill all entities", permission = "server.killall") +@Command(label = "killall", usage = "killall [sceneId]", permission = "server.killall") public final class KillAllCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.kill.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index f1e0f0f8c..f3fdb4998 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -13,10 +13,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, - description = "Kills the players current character", permission = "player.killcharacter") +@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter") public final class KillCharacterCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.killCharacter.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ListCommand.java b/src/main/java/emu/grasscutter/command/commands/ListCommand.java index bc35e65e1..7834d2467 100644 --- a/src/main/java/emu/grasscutter/command/commands/ListCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ListCommand.java @@ -10,10 +10,14 @@ import java.util.Map; import static emu.grasscutter.utils.Language.translate; -@Command(label = "list", usage = "list [uid]", - description = "List online players", aliases = {"players"}) +@Command(label = "list", usage = "list [uid]", aliases = {"players"}) public final class ListCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.list.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { Map<Integer, Player> playersMap = Grasscutter.getGameServer().getPlayers(); diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java index 69c8ce899..309451945 100644 --- a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -10,10 +10,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "permission", usage = "permission <add|remove> <permission>", - description = "Grants or removes a permission for a user", permission = "*") +@Command(label = "permission", usage = "permission <add|remove> <permission>", permission = "*") public final class PermissionCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.permission.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java index 7f6548c5b..3a3b40a3f 100644 --- a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java @@ -9,10 +9,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "position", usage = "position", aliases = {"pos"}, - description = "Get coordinates.") +@Command(label = "position", usage = "position", aliases = {"pos"}) public final class PositionCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.position.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 6c85d2024..8e3e5e5aa 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -9,10 +9,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "reload", usage = "reload", - description = "Reload server config", permission = "server.reload") +@Command(label = "reload", usage = "reload", permission = "server.reload") public final class ReloadCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.reload.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(sender, translate("commands.reload.reload_start")); diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java index 706fb95e0..9eba8e4c4 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -11,10 +11,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @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 String description() { + return translate("commands.resetConst.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index aeae0abbf..bba8da32c 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -9,9 +9,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "resetshop", usage = "resetshop", - description = "Reset target player's shop refresh time.", permission = "server.resetshop") +@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop") public final class ResetShopLimitCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.status.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/RestartCommand.java b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java index e3b8b2747..2c56ae443 100644 --- a/src/main/java/emu/grasscutter/command/commands/RestartCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java @@ -6,9 +6,16 @@ import emu.grasscutter.game.player.Player; import java.util.List; -@Command(label = "restart", usage = "restart - Restarts the current session") +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "restart", usage = "restart") public final class RestartCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.restart.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java index 838bea567..a56d68165 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java @@ -13,8 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @SuppressWarnings("ConstantConditions") -@Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", - description = "Sends mail to the specified user. The usage of this command changes based on it's composition state.", permission = "server.sendmail") +@Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", permission = "server.sendmail") public final class SendMailCommand implements CommandHandler { // TODO: You should be able to do /sendmail and then just send subsequent messages until you finish @@ -24,6 +23,11 @@ public final class SendMailCommand implements CommandHandler { // Key = User that is constructing the mail. private static final HashMap<Integer, MailBuilder> mailBeingConstructed = new HashMap<Integer, MailBuilder>(); + @Override + public String description() { + return translate("commands.sendMail.description"); + } + // Yes this is awful and I hate it. @Override public void execute(Player sender, Player targetPlayer, List<String> args) { @@ -40,7 +44,7 @@ public final class SendMailCommand implements CommandHandler { MailBuilder mailBuilder; switch (args.get(0).toLowerCase()) { case "help" -> { - CommandHandler.sendMessage(sender, this.getClass().getAnnotation(Command.class).description() + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); + CommandHandler.sendMessage(sender, this.description() + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); return; } case "all" -> mailBuilder = new MailBuilder(true, new Mail()); diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index acf63dea0..36e53de10 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -8,10 +8,15 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "say", usage = "say <message>", description = "Sends a message to a player as the server", +@Command(label = "say", usage = "say <message>", aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage") public final class SendMessageCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.sendMessage.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index 7184c679c..e098d99a5 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -12,10 +12,14 @@ import emu.grasscutter.server.packet.send.PacketAvatarFetterDataNotify; import static emu.grasscutter.utils.Language.translate; @Command(label = "setfetterlevel", usage = "setfetterlevel <level>", - description = "Sets your fetter level for your current active character", aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel") public final class SetFetterLevelCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.setFetterLevel.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index 233eb4d73..11cb8ab80 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -15,8 +15,7 @@ import emu.grasscutter.utils.Language; import static emu.grasscutter.utils.Language.translate; -@Command(label = "setstats", usage = "setstats|stats <stat> <value>", - description = "Set fight property for your current active character", aliases = {"stats"}, permission = "player.setstats") +@Command(label = "setstats", usage = "setstats|stats <stat> <value>", aliases = {"stats"}, permission = "player.setstats") public final class SetStatsCommand implements CommandHandler { static class Stat { String name; @@ -174,6 +173,11 @@ public final class SetStatsCommand implements CommandHandler { stats.put("_nonextra_physical_add_hurt", new Stat("NONEXTRA_PHYSICAL_ADD_HURT", FightProperty.FIGHT_PROP_NONEXTRA_PHYSICAL_ADD_HURT, true)); } + @Override + public String description() { + return translate("commands.setStats.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.ingame"); diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index 914d8cecc..16b10bc6c 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -10,10 +10,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "setworldlevel", usage = "setworldlevel <level>", - description = "Sets your world level (Relog to see proper effects)", aliases = {"setworldlvl"}, permission = "player.setworldlevel") public final class SetWorldLevelCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.setWorldLevel.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java index c66a45b50..8a995402b 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -22,10 +22,14 @@ import java.util.Random; import static emu.grasscutter.utils.Language.translate; -@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", - description = "Spawns an entity near you", permission = "server.spawn") +@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", permission = "server.spawn") public final class SpawnCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.spawn.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/StopCommand.java b/src/main/java/emu/grasscutter/command/commands/StopCommand.java index ad4903107..64326a748 100644 --- a/src/main/java/emu/grasscutter/command/commands/StopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/StopCommand.java @@ -9,10 +9,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "stop", usage = "stop", - description = "Stops the server", permission = "server.stop") +@Command(label = "stop", usage = "stop", permission = "server.stop") public final class StopCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.stop.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(null, translate("commands.stop.success")); diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index c9b2e8931..ca1bff76d 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -14,8 +14,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "talent", usage = "talent <talentID> <value>", - description = "Set talent level for your current active character", permission = "player.settalent") +@Command(label = "talent", usage = "talent <talentID> <value>", permission = "player.settalent") public final class TalentCommand implements CommandHandler { private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) { int oldLevel = avatar.getSkillLevelMap().get(talentId); @@ -44,6 +43,11 @@ public final class TalentCommand implements CommandHandler { CommandHandler.sendMessage(sender, translate(successMessage, talentLevel)); } + @Override + public String description() { + return translate("commands.talent.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index 54c6101f7..d25e73f96 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -10,9 +10,13 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "tpall", usage = "tpall", - description = "Teleports all players in your world to your position", permission = "player.tpall") +@Command(label = "tpall", usage = "tpall", permission = "player.tpall") public final class TeleportAllCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.teleportAll.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index 06b669a17..364e4188f 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -10,8 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, - description = "Change the player's position.", permission = "player.teleport") +@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport") public final class TeleportCommand implements CommandHandler { private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later @@ -25,6 +24,11 @@ public final class TeleportCommand implements CommandHandler { return current; } + @Override + public String description() { + return translate("commands.teleport.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index df8a6a01f..d2a6c5f64 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -11,10 +11,14 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "weather", usage = "weather <weatherId> [climateId]", - description = "Changes the weather.", aliases = {"w"}, permission = "player.weather") +@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather") public final class WeatherCommand implements CommandHandler { + @Override + public String description() { + return translate("commands.weather.description"); + } + @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index d9923a656..b5558833f 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -21,6 +21,7 @@ import com.google.gson.reflect.TypeToken; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; @@ -112,17 +113,16 @@ final class ToolsWithLanguageOption { writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator()); CommandMap cmdMap = new CommandMap(true); - List<Command> cmdList = new ArrayList<>(cmdMap.getAnnotationsAsList()); + HashMap<CommandHandler, Command> cmdList = cmdMap.getHandlersAndAnnotations(); writer.println("// Commands"); - for (Command cmd : cmdList) { - String cmdName = cmd.label(); + cmdList.forEach((handler, command) -> { + String cmdName = command.label(); while (cmdName.length() <= 15) { cmdName = " " + cmdName; } - writer.println(cmdName + " : " + cmd.description()); - } - + writer.println(cmdName + " : " + handler.description()); + }); writer.println(); list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index fb33ba287..438674e9c 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -95,17 +95,20 @@ "create": "Account created with UID %s.", "delete": "Account deleted.", "no_account": "Account not found.", - "command_usage": "Usage: account <create|delete> <username> [uid]" + "command_usage": "Usage: account <create|delete> <username> [uid]", + "description": "Modify user accounts" }, "broadcast": { "command_usage": "Usage: broadcast <message>", - "message_sent": "Message sent." + "message_sent": "Message sent.", + "description": "Sends a message to all the players" }, "changescene": { "usage": "Usage: changescene <sceneId>", "already_in_scene": "You are already in that scene.", "success": "Changed to scene %s.", - "exists_error": "The specified scene does not exist." + "exists_error": "The specified scene does not exist.", + "description": "Changes your scene" }, "clear": { "command_usage": "Usage: clear <all|wp|art|mat>", @@ -115,35 +118,41 @@ "furniture": "Cleared furniture for %s.", "displays": "Cleared displays for %s.", "virtuals": "Cleared virtuals for %s.", - "everything": "Cleared everything for %s." + "everything": "Cleared everything for %s.", + "description": "Deletes unequipped unlocked items, including yellow rarity ones from your inventory" }, "coop": { "usage": "Usage: coop <playerId> <target playerId>", - "success": "Summoned %s to %s's world." + "success": "Summoned %s to %s's world.", + "description": "Forces someone to join the world of others" }, "enter_dungeon": { "usage": "Usage: enterdungeon <dungeon id>", "changed": "Changed to dungeon %s", "not_found_error": "Dungeon does not exist", - "in_dungeon_error": "You are already in that dungeon" + "in_dungeon_error": "You are already in that dungeon", + "description": "Enter a dungeon" }, "giveAll": { "usage": "Usage: giveall [player] [amount]", "started": "Receiving all items...", "success": "Successfully gave all items to %s.", - "invalid_amount_or_playerId": "Invalid amount or player ID." + "invalid_amount_or_playerId": "Invalid amount or player ID.", + "description": "Gives all items" }, "giveArtifact": { "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", "id_error": "Invalid artifact ID.", - "success": "Given %s to %s." + "success": "Given %s to %s.", + "description": "Gives the player a specified artifact" }, "giveChar": { "usage": "Usage: givechar <player> <itemId|itemName> [amount]", "given": "Given %s with level %s to %s.", "invalid_avatar_id": "Invalid avatar id.", "invalid_avatar_level": "Invalid avatar level.", - "invalid_avatar_or_player_id": "Invalid avatar or player ID." + "invalid_avatar_or_player_id": "Invalid avatar or player ID.", + "description": "Gives the player a specified character" }, "give": { "usage": "Usage: give <player> <itemId|itemName> [amount] [level]", @@ -151,29 +160,36 @@ "refinement_must_between_1_and_5": "Refinement must be between 1 and 5.", "given": "Given %s of %s to %s.", "given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s", - "given_level": "Given %s with level %s %s times to %s" + "given_level": "Given %s with level %s %s times to %s", + "description": "Gives an item to you or the specified player" }, "godmode": { - "success": "Godmode is now %s for %s." + "success": "Godmode is now %s for %s.", + "description": "Prevents you from taking damage. Defaults to toggle." }, "heal": { - "success": "All characters have been healed." + "success": "All characters have been healed.", + "description": "Heal all characters in your current team." }, "kick": { "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", - "server_kick_player": "Kicking player [%s:%s]" + "server_kick_player": "Kicking player [%s:%s]", + "description": "Kicks the specified player from the server (WIP)" }, "kill": { "usage": "Usage: killall [playerUid] [sceneId]", "scene_not_found_in_player_world": "Scene not found in player world", - "kill_monsters_in_scene": "Killing %s monsters in scene %s" + "kill_monsters_in_scene": "Killing %s monsters in scene %s", + "description": "Kill all entities" }, "killCharacter": { "usage": "Usage: /killcharacter [playerId]", - "success": "Killed %s's current character." + "success": "Killed %s's current character.", + "description": "Kills the players current character" }, "list": { - "success": "There are %s player(s) online:" + "success": "There are %s player(s) online:", + "description": "List online players" }, "permission": { "usage": "Usage: permission <add|remove> <username> <permission>", @@ -181,21 +197,26 @@ "has_error": "They already have this permission!", "remove": "Permission removed.", "not_have_error": "They don't have this permission!", - "account_error": "The account cannot be found." + "account_error": "The account cannot be found.", + "description": "Grants or removes a permission for a user" }, "position": { - "success": "Coordinates: %s, %s, %s\nScene id: %s" + "success": "Coordinates: %s, %s, %s\nScene id: %s", + "description": "Get coordinates." }, "reload": { "reload_start": "Reloading config.", - "reload_done": "Reload complete." + "reload_done": "Reload complete.", + "description": "Reload server config" }, "resetConst": { "reset_all": "Reset all avatars' constellations.", - "success": "Constellations for %s have been reset. Please relog to see changes." + "success": "Constellations for %s have been reset. Please relog to see changes.", + "description": "Resets the constellation level on your current active character, will need to relog after using the command to see any changes." }, "resetShopLimit": { - "usage": "Usage: /resetshop <player id>" + "usage": "Usage: /resetshop <player id>", + "description": "Reset target player's shop refresh time." }, "sendMail": { "usage": "Usage: give [player] <itemId|itemName> [amount]", @@ -217,17 +238,20 @@ "message": "<message>", "sender": "<sender>", "arguments": "<itemId|itemName|finish> [amount] [level]", - "error": "ERROR: invalid construction stage %s. Check console for stacktrace." + "error": "ERROR: invalid construction stage %s. Check console for stacktrace.", + "description": "Sends mail to the specified user. The usage of this command changes based on it's composition state." }, "sendMessage": { "usage": "Usage: sendmessage <player> <message>", - "success": "Message sent." + "success": "Message sent.", + "description": "Sends a message to a player as the server" }, "setFetterLevel": { "usage": "Usage: setfetterlevel <level>", "range_error": "Fetter level must be between 0 and 10.", "success": "Fetter level set to %s", - "level_error": "Invalid fetter level." + "level_error": "Invalid fetter level.", + "description": "Sets your fetter level for your current active character" }, "setStats": { "usage_console": "Usage: setstats|stats @<UID> <stat> <value>", @@ -238,20 +262,24 @@ "player_error": "Player not found or offline.", "set_self": "%s set to %s.", "set_for_uid": "%s for %s set to %s.", - "set_max_hp": "MAX HP set to %s." + "set_max_hp": "MAX HP set to %s.", + "description": "Set fight property for your current active character" }, "setWorldLevel": { "usage": "Usage: setworldlevel <level>", "value_error": "World level must be between 0-8", "success": "World level set to %s.", - "invalid_world_level": "Invalid world level." + "invalid_world_level": "Invalid world level.", + "description": "Sets your world level (Relog to see proper effects)" }, "spawn": { "usage": "Usage: spawn <entityId> [amount] [level(monster only)]", - "success": "Spawned %s of %s." + "success": "Spawned %s of %s.", + "description": "Spawns an entity near you" }, "stop": { - "success": "Server shutting down..." + "success": "Server shutting down...", + "description": "Stops the server" }, "talent": { "usage_1": "To set talent level: /talent set <talentID> <value>", @@ -267,32 +295,41 @@ "invalid_level": "Invalid talent level.", "normal_attack_id": "Normal Attack ID %s.", "e_skill_id": "E skill ID %s.", - "q_skill_id": "Q skill ID %s." + "q_skill_id": "Q skill ID %s.", + "description": "Set talent level for your current active character" }, "teleportAll": { "success": "Summoned all players to your location.", - "error": "You only can use this command in MP mode." + "error": "You only can use this command in MP mode.", + "description": "Teleports all players in your world to your position" }, "teleport": { "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", "specify_player_id": "You must specify a player id.", "invalid_position": "Invalid position.", - "success": "Teleported %s to %s, %s, %s in scene %s" + "success": "Teleported %s to %s, %s, %s in scene %s", + "description": "Change the player's position." }, "weather": { "usage": "Usage: weather <weatherId> [climateId]", "success": "Changed weather to %s with climate %s", - "invalid_id": "Invalid ID." + "invalid_id": "Invalid ID.", + "description": "Changes the weather." }, "drop": { "command_usage": "Usage: drop <itemId|itemName> [amount]", - "success": "Dropped %s of %s." + "success": "Dropped %s of %s.", + "description": "Drops an item near you" }, "help": { "usage": "Usage: ", "aliases": "Aliases: ", - "available_commands": "Available commands: " + "available_commands": "Available commands: ", + "description": "Sends the help message or shows information about a specified command" + }, + "restart": { + "description": "Restarts the current session" } } } From f49862145cd26306e0b80e87e776e771e3f1f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= <fkj@fkj233.cn> Date: Sun, 8 May 2022 11:51:32 +0800 Subject: [PATCH 197/434] add Command description --- src/main/java/emu/grasscutter/command/Command.java | 2 ++ src/main/java/emu/grasscutter/command/CommandHandler.java | 2 +- .../java/emu/grasscutter/command/commands/HelpCommand.java | 6 +++--- src/main/java/emu/grasscutter/tools/Tools.java | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/Command.java b/src/main/java/emu/grasscutter/command/Command.java index c94804678..734f454ea 100644 --- a/src/main/java/emu/grasscutter/command/Command.java +++ b/src/main/java/emu/grasscutter/command/Command.java @@ -9,6 +9,8 @@ public @interface Command { 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 index dadb87bb3..76d5d7b04 100644 --- a/src/main/java/emu/grasscutter/command/CommandHandler.java +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -7,7 +7,7 @@ import java.util.List; public interface CommandHandler { - String description(); + default String description() { return null; }; /** * Send a message to the target. diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java index dbb85bf9d..323c2878b 100644 --- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -43,7 +43,7 @@ public final class HelpCommand implements CommandHandler { } else { Command annotation = handler.getClass().getAnnotation(Command.class); - builder.append(" ").append(handler.description()).append("\n"); + builder.append(" ").append(handler.description() == null ? annotation.description(): handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -65,7 +65,7 @@ public final class HelpCommand implements CommandHandler { StringBuilder builder = new StringBuilder("\n" + translate("commands.help.available_commands") + "\n"); annotations.forEach((annotation, handler) -> { builder.append(annotation.label()).append("\n"); - builder.append(" ").append(handler.description()).append("\n"); + builder.append(" ").append(handler.description() == null ? annotation.description() : handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -82,7 +82,7 @@ public final class HelpCommand implements CommandHandler { CommandHandler.sendMessage(player, translate("commands.help.available_commands")); annotations.forEach((annotation, handler) -> { StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); - builder.append(" ").append(handler.description()).append("\n"); + builder.append(" ").append(handler.description() == null ? annotation.description() : handler.description()).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index b5558833f..a1bcb1b9a 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -121,7 +121,7 @@ final class ToolsWithLanguageOption { while (cmdName.length() <= 15) { cmdName = " " + cmdName; } - writer.println(cmdName + " : " + handler.description()); + writer.println(cmdName + " : " + (handler.description() == null ? command.description() : handler.description())); }); writer.println(); From b3317bd6d75c7a23d3004556ba73f25323cc7eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= <fkj@fkj233.cn> Date: Sun, 8 May 2022 14:44:14 +0800 Subject: [PATCH 198/434] Using annotation key translation --- .../java/emu/grasscutter/command/Command.java | 2 +- .../grasscutter/command/CommandHandler.java | 2 -- .../emu/grasscutter/command/CommandMap.java | 8 ------- .../command/commands/AccountCommand.java | 7 +----- .../command/commands/BroadcastCommand.java | 7 +----- .../command/commands/ChangeSceneCommand.java | 6 +---- .../command/commands/ClearCommand.java | 6 +---- .../command/commands/CoopCommand.java | 6 +---- .../command/commands/DropCommand.java | 7 +----- .../command/commands/EnterDungeonCommand.java | 6 +---- .../command/commands/GiveAllCommand.java | 7 +----- .../command/commands/GiveArtifactCommand.java | 6 +---- .../command/commands/GiveCharCommand.java | 7 +----- .../command/commands/GiveCommand.java | 7 +----- .../command/commands/GodModeCommand.java | 7 +----- .../command/commands/HealCommand.java | 6 +---- .../command/commands/HelpCommand.java | 23 ++++++++----------- .../command/commands/KickCommand.java | 7 +----- .../command/commands/KillAllCommand.java | 7 +----- .../commands/KillCharacterCommand.java | 7 +----- .../command/commands/ListCommand.java | 7 +----- .../command/commands/PermissionCommand.java | 7 +----- .../command/commands/PositionCommand.java | 7 +----- .../command/commands/ReloadCommand.java | 7 +----- .../command/commands/ResetConstCommand.java | 7 +----- .../commands/ResetShopLimitCommand.java | 6 +---- .../command/commands/RestartCommand.java | 7 +----- .../command/commands/SendMailCommand.java | 9 ++------ .../command/commands/SendMessageCommand.java | 7 +----- .../commands/SetFetterLevelCommand.java | 7 +----- .../command/commands/SetStatsCommand.java | 7 +----- .../commands/SetWorldLevelCommand.java | 7 +----- .../command/commands/SpawnCommand.java | 7 +----- .../command/commands/StopCommand.java | 7 +----- .../command/commands/TalentCommand.java | 7 +----- .../command/commands/TeleportAllCommand.java | 6 +---- .../command/commands/TeleportCommand.java | 7 +----- .../command/commands/WeatherCommand.java | 7 +----- .../java/emu/grasscutter/tools/Tools.java | 12 ++++++---- src/main/resources/languages/en-US.json | 1 + 40 files changed, 53 insertions(+), 227 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/Command.java b/src/main/java/emu/grasscutter/command/Command.java index 734f454ea..045f6a51c 100644 --- a/src/main/java/emu/grasscutter/command/Command.java +++ b/src/main/java/emu/grasscutter/command/Command.java @@ -9,7 +9,7 @@ public @interface Command { String usage() default "No usage specified"; - String description() default "No description specified"; + String description() default "commands.generic.no_description_specified"; String[] aliases() default {}; diff --git a/src/main/java/emu/grasscutter/command/CommandHandler.java b/src/main/java/emu/grasscutter/command/CommandHandler.java index 76d5d7b04..f4fe12b3f 100644 --- a/src/main/java/emu/grasscutter/command/CommandHandler.java +++ b/src/main/java/emu/grasscutter/command/CommandHandler.java @@ -7,8 +7,6 @@ import java.util.List; public interface CommandHandler { - default String description() { return null; }; - /** * Send a message to the target. * diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index 6b6a852af..a183c6ac3 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -85,14 +85,6 @@ public final class CommandMap { return new LinkedHashMap<>(this.annotations); } - public HashMap<CommandHandler, Command> getHandlersAndAnnotations() { - HashMap<CommandHandler, Command> hashMap = new HashMap<>(); - this.commands.forEach((key, handler) -> { - hashMap.put(handler, this.annotations.get(key)); - }); - return hashMap; - } - /** * Returns a list of all registered commands. * diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 1c5c8228e..4b287afa7 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "account", usage = "account <create|delete> <username> [uid]") +@Command(label = "account", usage = "account <create|delete> <username> [uid]", description = "commands.account.description") public final class AccountCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.account.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender != null) { diff --git a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java index fcabb131c..95f0c7c05 100644 --- a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "broadcast", usage = "broadcast <message>", aliases = {"b"}, permission = "server.broadcast") +@Command(label = "broadcast", usage = "broadcast <message>", aliases = {"b"}, permission = "server.broadcast", description = "commands.broadcast.description") public final class BroadcastCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.broadcast.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (args.size() < 1) { diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index ea7bfe0f6..594eb27c6 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -8,12 +8,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "changescene", usage = "changescene <scene id>", aliases = {"scene"}, permission = "player.changescene") +@Command(label = "changescene", usage = "changescene <scene id>", aliases = {"scene"}, permission = "player.changescene", description = "commands.changescene.description") public final class ChangeSceneCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.changescene.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java index d416bec52..38f78e638 100644 --- a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java @@ -13,15 +13,11 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "clear", usage = "clear <all|wp|art|mat>", //Merged /clearartifacts and /clearweapons to /clear <args> [uid] + description = "commands.clear.description", aliases = {"clear"}, permission = "player.clearinv") public final class ClearCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.clear.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index 120665b76..cf7d4fc82 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -9,12 +9,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "coop", usage = "coop [host UID]", permission = "server.coop") +@Command(label = "coop", usage = "coop [host UID]", permission = "server.coop", description = "commands.coop.description") public final class CoopCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.coop.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java index a187cfda8..10c306cad 100644 --- a/src/main/java/emu/grasscutter/command/commands/DropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -13,14 +13,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", aliases = {"d", "dropitem"}, permission = "server.drop") +@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", aliases = {"d", "dropitem"}, permission = "server.drop", description = "commands.drop.description") public final class DropCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.drop.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java index d92d537e9..8534c034f 100644 --- a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java @@ -8,12 +8,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", aliases = {"dungeon"}, permission = "player.enterdungeon") +@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", aliases = {"dungeon"}, permission = "player.enterdungeon", description = "commands.enter_dungeon.description") public final class EnterDungeonCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.enter_dungeon.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index 5009a4462..c94d67129 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -15,14 +15,9 @@ import java.util.*; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveall", usage = "giveall [amount]", aliases = {"givea"}, permission = "player.giveall", threading = true) +@Command(label = "giveall", usage = "giveall [amount]", aliases = {"givea"}, permission = "player.giveall", threading = true, description = "commands.giveAll.description") public final class GiveAllCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.giveAll.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index d41e83671..b87642bb2 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -16,12 +16,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart") +@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart", description = "commands.giveArtifact.description") public final class GiveArtifactCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.giveArtifact.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 0e784639d..5c6bad0d2 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -12,14 +12,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "givechar", usage = "givechar <avatarId> [level]", aliases = {"givec"}, permission = "player.givechar") +@Command(label = "givechar", usage = "givechar <avatarId> [level]", aliases = {"givec"}, permission = "player.givechar", description = "commands.giveChar.description") public final class GiveCharCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.giveChar.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index 3daa2dff3..19a9a8d26 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; import static emu.grasscutter.utils.Language.translate; @Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", aliases = { - "g", "item", "giveitem"}, permission = "player.give") + "g", "item", "giveitem"}, permission = "player.give", description = "commands.give.description") public final class GiveCommand implements CommandHandler { Pattern lvlRegex = Pattern.compile("l(?:vl?)?(\\d+)"); // Java is a joke of a proglang that doesn't have raw string literals Pattern refineRegex = Pattern.compile("r(\\d+)"); @@ -31,11 +31,6 @@ public final class GiveCommand implements CommandHandler { return -1; } - @Override - public String description() { - return translate("commands.give.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java index 4fd2999a6..bf2a00c9f 100644 --- a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -8,14 +8,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "godmode", usage = "godmode [on|off|toggle]", permission = "player.godmode") +@Command(label = "godmode", usage = "godmode [on|off|toggle]", permission = "player.godmode", description = "commands.godmode.description") public final class GodModeCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.godmode.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java index b459ecb8c..440db0a49 100644 --- a/src/main/java/emu/grasscutter/command/commands/HealCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -11,12 +11,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "heal", usage = "heal|h", aliases = {"h"}, permission = "player.heal") +@Command(label = "heal", usage = "heal|h", aliases = {"h"}, permission = "player.heal", description = "commands.heal.description") public final class HealCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.heal.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java index 323c2878b..8a222f7a6 100644 --- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -10,26 +10,21 @@ import java.util.*; import static emu.grasscutter.utils.Language.translate; -@Command(label = "help", usage = "help [command]") +@Command(label = "help", usage = "help [command]", description = "commands.help.description") public final class HelpCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.help.description"); - } - @Override public void execute(Player player, Player targetPlayer, List<String> args) { if (args.size() < 1) { HashMap<String, CommandHandler> handlers = CommandMap.getInstance().getHandlers(); - HashMap<Command, CommandHandler> annotations = new HashMap<>(); + List<Command> 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.put(annotation, handlers.get(key)); + annotations.add(annotation); } } @@ -43,7 +38,7 @@ public final class HelpCommand implements CommandHandler { } else { Command annotation = handler.getClass().getAnnotation(Command.class); - builder.append(" ").append(handler.description() == null ? annotation.description(): handler.description()).append("\n"); + builder.append(" ").append(translate(annotation.description())).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -60,12 +55,12 @@ public final class HelpCommand implements CommandHandler { } } - void SendAllHelpMessage(Player player, HashMap<Command, CommandHandler> annotations) { + void SendAllHelpMessage(Player player, List<Command> annotations) { if (player == null) { StringBuilder builder = new StringBuilder("\n" + translate("commands.help.available_commands") + "\n"); - annotations.forEach((annotation, handler) -> { + annotations.forEach(annotation -> { builder.append(annotation.label()).append("\n"); - builder.append(" ").append(handler.description() == null ? annotation.description() : handler.description()).append("\n"); + builder.append(" ").append(translate(annotation.description())).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); @@ -80,9 +75,9 @@ public final class HelpCommand implements CommandHandler { CommandHandler.sendMessage(null, builder.toString()); } else { CommandHandler.sendMessage(player, translate("commands.help.available_commands")); - annotations.forEach((annotation, handler) -> { + annotations.forEach(annotation -> { StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); - builder.append(" ").append(handler.description() == null ? annotation.description() : handler.description()).append("\n"); + builder.append(" ").append(translate(annotation.description())).append("\n"); builder.append(translate("commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { builder.append("\n").append(translate("commands.help.aliases")); diff --git a/src/main/java/emu/grasscutter/command/commands/KickCommand.java b/src/main/java/emu/grasscutter/command/commands/KickCommand.java index 71b487cc4..9741226e7 100644 --- a/src/main/java/emu/grasscutter/command/commands/KickCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KickCommand.java @@ -8,14 +8,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "kick", usage = "kick", permission = "server.kick") +@Command(label = "kick", usage = "kick", permission = "server.kick", description = "commands.kick.description") public final class KickCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.kick.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java index 1fa51eec4..da9ac7b5e 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -12,14 +12,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killall", usage = "killall [sceneId]", permission = "server.killall") +@Command(label = "killall", usage = "killall [sceneId]", permission = "server.killall", description = "commands.kill.description") public final class KillAllCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.kill.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index f3fdb4998..3eda6f7e7 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -13,14 +13,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter") +@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", description = "commands.list.description") public final class KillCharacterCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.killCharacter.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ListCommand.java b/src/main/java/emu/grasscutter/command/commands/ListCommand.java index 7834d2467..53a274e52 100644 --- a/src/main/java/emu/grasscutter/command/commands/ListCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ListCommand.java @@ -10,14 +10,9 @@ import java.util.Map; import static emu.grasscutter.utils.Language.translate; -@Command(label = "list", usage = "list [uid]", aliases = {"players"}) +@Command(label = "list", usage = "list [uid]", aliases = {"players"}, description = "commands.list.description") public final class ListCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.list.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { Map<Integer, Player> playersMap = Grasscutter.getGameServer().getPlayers(); diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java index 309451945..4b945b3d1 100644 --- a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -10,14 +10,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "permission", usage = "permission <add|remove> <permission>", permission = "*") +@Command(label = "permission", usage = "permission <add|remove> <permission>", permission = "*", description = "commands.permission.description") public final class PermissionCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.permission.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java index 3a3b40a3f..b5a250af6 100644 --- a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "position", usage = "position", aliases = {"pos"}) +@Command(label = "position", usage = "position", aliases = {"pos"}, description = "commands.position.description") public final class PositionCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.position.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 8e3e5e5aa..29eb93d0d 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "reload", usage = "reload", permission = "server.reload") +@Command(label = "reload", usage = "reload", permission = "server.reload", description = "commands.reload.description") public final class ReloadCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.reload.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(sender, translate("commands.reload.reload_start")); diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java index 9eba8e4c4..3a77cee4d 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -11,14 +11,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "resetconst", usage = "resetconst [all]", - aliases = {"resetconstellation"}, permission = "player.resetconstellation") + aliases = {"resetconstellation"}, permission = "player.resetconstellation", description = "commands.resetConst.description") public final class ResetConstCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.resetConst.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index bba8da32c..7aa84ff6a 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -9,12 +9,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop") +@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", description = "commands.status.description") public final class ResetShopLimitCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.status.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/RestartCommand.java b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java index 2c56ae443..045a49d9e 100644 --- a/src/main/java/emu/grasscutter/command/commands/RestartCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/RestartCommand.java @@ -8,14 +8,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "restart", usage = "restart") +@Command(label = "restart", usage = "restart", description = "commands.restart.description") public final class RestartCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.restart.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java index a56d68165..69aafa20b 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java @@ -13,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @SuppressWarnings("ConstantConditions") -@Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", permission = "server.sendmail") +@Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", permission = "server.sendmail", description = "commands.sendMail.description") public final class SendMailCommand implements CommandHandler { // TODO: You should be able to do /sendmail and then just send subsequent messages until you finish @@ -23,11 +23,6 @@ public final class SendMailCommand implements CommandHandler { // Key = User that is constructing the mail. private static final HashMap<Integer, MailBuilder> mailBeingConstructed = new HashMap<Integer, MailBuilder>(); - @Override - public String description() { - return translate("commands.sendMail.description"); - } - // Yes this is awful and I hate it. @Override public void execute(Player sender, Player targetPlayer, List<String> args) { @@ -44,7 +39,7 @@ public final class SendMailCommand implements CommandHandler { MailBuilder mailBuilder; switch (args.get(0).toLowerCase()) { case "help" -> { - CommandHandler.sendMessage(sender, this.description() + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); + CommandHandler.sendMessage(sender, translate(this.getClass().getAnnotation(Command.class).description()) + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); return; } case "all" -> mailBuilder = new MailBuilder(true, new Mail()); diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index 36e53de10..18d6264db 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "say", usage = "say <message>", - aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage") + aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage", description = "commands.sendMessage.description") public final class SendMessageCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.sendMessage.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index e098d99a5..ca5a3cb43 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -12,14 +12,9 @@ import emu.grasscutter.server.packet.send.PacketAvatarFetterDataNotify; import static emu.grasscutter.utils.Language.translate; @Command(label = "setfetterlevel", usage = "setfetterlevel <level>", - aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel") + aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel", description = "commands.setFetterLevel.description") public final class SetFetterLevelCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.setFetterLevel.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index 11cb8ab80..c7ed78a58 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -15,7 +15,7 @@ import emu.grasscutter.utils.Language; import static emu.grasscutter.utils.Language.translate; -@Command(label = "setstats", usage = "setstats|stats <stat> <value>", aliases = {"stats"}, permission = "player.setstats") +@Command(label = "setstats", usage = "setstats|stats <stat> <value>", aliases = {"stats"}, permission = "player.setstats", description = "commands.setStats.description") public final class SetStatsCommand implements CommandHandler { static class Stat { String name; @@ -173,11 +173,6 @@ public final class SetStatsCommand implements CommandHandler { stats.put("_nonextra_physical_add_hurt", new Stat("NONEXTRA_PHYSICAL_ADD_HURT", FightProperty.FIGHT_PROP_NONEXTRA_PHYSICAL_ADD_HURT, true)); } - @Override - public String description() { - return translate("commands.setStats.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.ingame"); diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index 16b10bc6c..41b959336 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -10,14 +10,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "setworldlevel", usage = "setworldlevel <level>", - aliases = {"setworldlvl"}, permission = "player.setworldlevel") + aliases = {"setworldlvl"}, permission = "player.setworldlevel", description = "commands.setWorldLevel.description") public final class SetWorldLevelCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.setWorldLevel.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java index 8a995402b..7f0c704c6 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -22,14 +22,9 @@ import java.util.Random; import static emu.grasscutter.utils.Language.translate; -@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", permission = "server.spawn") +@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", permission = "server.spawn", description = "commands.spawn.description") public final class SpawnCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.spawn.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/StopCommand.java b/src/main/java/emu/grasscutter/command/commands/StopCommand.java index 64326a748..129b27b24 100644 --- a/src/main/java/emu/grasscutter/command/commands/StopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/StopCommand.java @@ -9,14 +9,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "stop", usage = "stop", permission = "server.stop") +@Command(label = "stop", usage = "stop", permission = "server.stop", description = "commands.stop.description") public final class StopCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.stop.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(null, translate("commands.stop.success")); diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index ca1bff76d..1540a81f8 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -14,7 +14,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "talent", usage = "talent <talentID> <value>", permission = "player.settalent") +@Command(label = "talent", usage = "talent <talentID> <value>", permission = "player.settalent", description = "commands.talent.description") public final class TalentCommand implements CommandHandler { private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) { int oldLevel = avatar.getSkillLevelMap().get(talentId); @@ -43,11 +43,6 @@ public final class TalentCommand implements CommandHandler { CommandHandler.sendMessage(sender, translate(successMessage, talentLevel)); } - @Override - public String description() { - return translate("commands.talent.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index d25e73f96..175f69b81 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -10,12 +10,8 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "tpall", usage = "tpall", permission = "player.tpall") +@Command(label = "tpall", usage = "tpall", permission = "player.tpall", description = "commands.teleportAll.description") public final class TeleportAllCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.teleportAll.description"); - } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index 364e4188f..0d15b55af 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -10,7 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport") +@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport", description = "commands.teleport.description") public final class TeleportCommand implements CommandHandler { private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later @@ -24,11 +24,6 @@ public final class TeleportCommand implements CommandHandler { return current; } - @Override - public String description() { - return translate("commands.teleport.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index d2a6c5f64..dd0002790 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -11,14 +11,9 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather") +@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather", description = "commands.weather.description") public final class WeatherCommand implements CommandHandler { - @Override - public String description() { - return translate("commands.weather.description"); - } - @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index a1bcb1b9a..f69aafc80 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -31,6 +31,8 @@ import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.SceneData; import emu.grasscutter.utils.Utils; +import static emu.grasscutter.utils.Language.translate; + public final class Tools { public static void createGmHandbook() throws Exception { ToolsWithLanguageOption.createGmHandbook(getLanguageOption()); @@ -113,16 +115,16 @@ final class ToolsWithLanguageOption { writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator()); CommandMap cmdMap = new CommandMap(true); - HashMap<CommandHandler, Command> cmdList = cmdMap.getHandlersAndAnnotations(); + List<Command> cmdList = new ArrayList<>(cmdMap.getAnnotationsAsList()); writer.println("// Commands"); - cmdList.forEach((handler, command) -> { - String cmdName = command.label(); + for (Command cmd : cmdList) { + String cmdName = cmd.label(); while (cmdName.length() <= 15) { cmdName = " " + cmdName; } - writer.println(cmdName + " : " + (handler.description() == null ? command.description() : handler.description())); - }); + writer.println(cmdName + " : " + translate(cmd.description())); + } writer.println(); list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 438674e9c..9a7485608 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -56,6 +56,7 @@ "console_execute_error": "This command can only be run from the console.", "player_execute_error": "Run this command in-game.", "command_exist_error": "No command found.", + "no_description_specified": "No description specified", "invalid": { "amount": "Invalid amount.", "artifactId": "Invalid artifactId.", From 8a52a041bd3de7eaa213688c787bd2dc4ec8f5e1 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sun, 8 May 2022 17:11:02 +0800 Subject: [PATCH 199/434] Persist Tower Data && Set The Tower Schedule --- build.gradle | 2 +- data/TowerSchedule.json | 5 ++ .../command/commands/UnlockTowerCommand.java | 32 +++++++ .../java/emu/grasscutter/data/GameData.java | 4 + .../data/def/TowerScheduleData.java | 70 ++++++++++++++++ .../dungeons/TowerDungeonSettleListener.java | 16 ++-- .../game/tower/TowerLevelRecord.java | 64 ++++++++++++++ .../grasscutter/game/tower/TowerManager.java | 83 +++++++++++++++---- .../game/tower/TowerScheduleConfig.java | 35 ++++++++ .../game/tower/TowerScheduleManager.java | 75 +++++++++++++++++ .../scripts/SceneScriptManager.java | 29 ++++--- .../emu/grasscutter/scripts/ScriptLib.java | 32 ++++--- .../grasscutter/server/game/GameServer.java | 8 +- .../packet/recv/HandlerTowerAllDataReq.java | 5 +- .../send/PacketDungeonSettleNotify.java | 2 +- .../packet/send/PacketTowerAllDataRsp.java | 57 +++++++++---- .../PacketTowerFloorRecordChangeNotify.java | 6 +- .../send/PacketTowerLevelStarCondNotify.java | 32 +++++++ .../emu/grasscutter/utils/DateHelper.java | 6 +- src/main/resources/languages/en-US.json | 3 + 20 files changed, 500 insertions(+), 66 deletions(-) create mode 100644 data/TowerSchedule.json create mode 100644 src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java create mode 100644 src/main/java/emu/grasscutter/data/def/TowerScheduleData.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java diff --git a/build.gradle b/build.gradle index 8c9257777..2f19e9c99 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' diff --git a/data/TowerSchedule.json b/data/TowerSchedule.json new file mode 100644 index 000000000..b93100645 --- /dev/null +++ b/data/TowerSchedule.json @@ -0,0 +1,5 @@ +{ + "scheduleId" : 1, + "scheduleStartTime" : "2022-05-01T00:00:00+08:00", + "nextScheduleChangeTime" : "2022-05-30T00:00:00+08:00" +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java new file mode 100644 index 000000000..e0fce695c --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java @@ -0,0 +1,32 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.tower.TowerLevelRecord; + +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "unlocktower", usage = "unlocktower", aliases = {"ut"}, + description = "Unlock all levels of tower", permission = "player.tower") +public class UnlockTowerCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List<String> args) { + unlockFloor(sender, sender.getServer().getTowerScheduleManager() + .getCurrentTowerScheduleData().getEntranceFloorId()); + + unlockFloor(sender, sender.getServer().getTowerScheduleManager() + .getScheduleFloors()); + + CommandHandler.sendMessage(sender, translate("commands.tower.unlock_done")); + } + + public void unlockFloor(Player player, List<Integer> floors){ + floors.stream() + .filter(id -> !player.getTowerManager().getRecordMap().containsKey(id)) + .forEach(id -> player.getTowerManager().getRecordMap().put(id, new TowerLevelRecord(id))); + } +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 76a7f1652..fb3991ad7 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -70,6 +70,7 @@ public class GameData { private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap<TowerScheduleData> towerScheduleDataMap = new Int2ObjectOpenHashMap<>(); // Cache private static Map<Integer, List<Integer>> fetters = new HashMap<>(); @@ -320,4 +321,7 @@ public class GameData { public static Int2ObjectMap<TowerLevelData> getTowerLevelDataMap(){ return towerLevelDataMap; } + public static Int2ObjectMap<TowerScheduleData> getTowerScheduleDataMap(){ + return towerScheduleDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java b/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java new file mode 100644 index 000000000..017776c06 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java @@ -0,0 +1,70 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +import java.util.List; + +@ResourceType(name = "TowerScheduleExcelConfigData.json") +public class TowerScheduleData extends GameResource { + private int ScheduleId; + private List<Integer> EntranceFloorId; + private List<ScheduleDetail> Schedules; + private int MonthlyLevelConfigId; + @Override + public int getId() { + return ScheduleId; + } + + @Override + public void onLoad() { + super.onLoad(); + this.Schedules = this.Schedules.stream() + .filter(item -> item.getFloorList().size() > 0) + .toList(); + } + + public int getScheduleId() { + return ScheduleId; + } + + public void setScheduleId(int scheduleId) { + ScheduleId = scheduleId; + } + + public List<Integer> getEntranceFloorId() { + return EntranceFloorId; + } + + public void setEntranceFloorId(List<Integer> entranceFloorId) { + EntranceFloorId = entranceFloorId; + } + + public List<ScheduleDetail> getSchedules() { + return Schedules; + } + + public void setSchedules(List<ScheduleDetail> schedules) { + Schedules = schedules; + } + + public int getMonthlyLevelConfigId() { + return MonthlyLevelConfigId; + } + + public void setMonthlyLevelConfigId(int monthlyLevelConfigId) { + MonthlyLevelConfigId = monthlyLevelConfigId; + } + + public static class ScheduleDetail{ + private List<Integer> FloorList; + + public List<Integer> getFloorList() { + return FloorList; + } + + public void setFloorList(List<Integer> floorList) { + FloorList = floorList; + } + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java index 5b1ff7a30..c480d047f 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -12,12 +12,18 @@ public class TowerDungeonSettleListener implements DungeonSettleListener { scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); var towerManager = scene.getPlayers().get(0).getTowerManager(); - towerManager.notifyCurLevelRecordChangeWhenDone(); - scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify(towerManager.getCurrentFloorId())); - scene.broadcastPacket(new PacketDungeonSettleNotify(scene.getChallenge(), - true, + towerManager.notifyCurLevelRecordChangeWhenDone(3); + scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify( + towerManager.getCurrentFloorId(), + 3, + towerManager.canEnterScheduleFloor() + )); + + scene.broadcastPacket(new PacketDungeonSettleNotify( + scene.getChallenge(), + towerManager.hasNextFloor(), towerManager.hasNextLevel(), - towerManager.getNextFloorId() + towerManager.hasNextLevel() ? 0 : towerManager.getNextFloorId() )); } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java b/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java new file mode 100644 index 000000000..5a65f63ed --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java @@ -0,0 +1,64 @@ +package emu.grasscutter.game.tower; + +import dev.morphia.annotations.Entity; + +import java.util.HashMap; +import java.util.Map; + +@Entity +public class TowerLevelRecord { + /** + * floorId in config + */ + private int floorId; + /** + * LevelId - Stars + */ + private Map<Integer, Integer> passedLevelMap; + + private int floorStarRewardProgress; + + public TowerLevelRecord setLevelStars(int levelId, int stars){ + passedLevelMap.put(levelId, stars); + return this; + } + + public int getStarCount() { + return passedLevelMap.values().stream().mapToInt(Integer::intValue).sum(); + } + + public TowerLevelRecord(){ + + } + + public TowerLevelRecord(int floorId){ + this.floorId = floorId; + this.passedLevelMap = new HashMap<>(); + this.floorStarRewardProgress = 0; + } + + public int getFloorId() { + return floorId; + } + + public void setFloorId(int floorId) { + this.floorId = floorId; + } + + public Map<Integer, Integer> getPassedLevelMap() { + return passedLevelMap; + } + + public void setPassedLevelMap(Map<Integer, Integer> passedLevelMap) { + this.passedLevelMap = passedLevelMap; + } + + public int getFloorStarRewardProgress() { + return floorStarRewardProgress; + } + + public void setFloorStarRewardProgress(int floorStarRewardProgress) { + this.floorStarRewardProgress = floorStarRewardProgress; + } + +} diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 409549a1f..9346ffead 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -9,10 +9,12 @@ import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; - import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; +import emu.grasscutter.server.packet.send.PacketTowerLevelStarCondNotify; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Entity public class TowerManager { @@ -26,11 +28,19 @@ public class TowerManager { this.player = player; } + /** + * the floor players chose + */ private int currentFloorId; private int currentLevel; @Transient private int currentLevelId; + /** + * floorId - Record + */ + private Map<Integer, TowerLevelRecord> recordMap; + @Transient private int entryScene; @@ -38,7 +48,26 @@ public class TowerManager { return currentFloorId; } + public int getCurrentLevelId(){ + return this.currentLevelId + currentLevel; + } + + /** + * form 1-3 + */ + public int getCurrentLevel(){ + return currentLevel + 1; + } private static final List<DungeonSettleListener> towerDungeonSettleListener = List.of(new TowerDungeonSettleListener()); + + public Map<Integer, TowerLevelRecord> getRecordMap() { + if(recordMap == null){ + recordMap = new HashMap<>(); + recordMap.put(1001, new TowerLevelRecord(1001)); + } + return recordMap; + } + public void teamSelect(int floor, List<List<Long>> towerTeams) { var floorData = GameData.getTowerFloorDataMap().get(floor); @@ -54,51 +83,73 @@ public class TowerManager { entryScene = player.getSceneId(); } - player.getTeamManager().setupTemporaryTeam(towerTeams); } public void enterLevel(int enterPointId) { - var levelData = GameData.getTowerLevelDataMap().get(currentLevelId + currentLevel); + var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId()); - this.currentLevel++; - var id = levelData.getDungeonId(); + var dungeonId = levelData.getDungeonId(); notifyCurLevelRecordChange(); // use team user choose player.getTeamManager().useTemporaryTeam(0); - player.getServer().getDungeonManager().handoffDungeon(player, id, + player.getServer().getDungeonManager().handoffDungeon(player, dungeonId, towerDungeonSettleListener); // make sure user can exit dungeon correctly player.getScene().setPrevScene(entryScene); player.getScene().setPrevScenePoint(enterPointId); - player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, currentLevel)); + player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, getCurrentLevel())); // stop using skill player.getSession().send(new PacketCanUseSkillNotify(false)); + // notify the cond of stars + player.getSession().send(new PacketTowerLevelStarCondNotify(currentFloorId, getCurrentLevel())); } public void notifyCurLevelRecordChange(){ - player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel)); + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, getCurrentLevel())); } - public void notifyCurLevelRecordChangeWhenDone(){ - player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel + 1)); + public void notifyCurLevelRecordChangeWhenDone(int stars){ + if(!recordMap.containsKey(currentFloorId)){ + recordMap.put(currentFloorId, + new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(),stars)); + }else{ + recordMap.put(currentFloorId, + recordMap.get(currentFloorId).setLevelStars(getCurrentLevelId(),stars)); + } + + this.currentLevel++; + + if(!hasNextLevel()){ + // set up the next floor + recordMap.put(getNextFloorId(), new TowerLevelRecord(getNextFloorId())); + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(getNextFloorId(), 1)); + }else{ + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, getCurrentLevel())); + } } public boolean hasNextLevel(){ return this.currentLevel < 3; } - public int getNextFloorId() { - if(hasNextLevel()){ - return 0; - } - this.currentFloorId++; - return this.currentFloorId; + return player.getServer().getTowerScheduleManager().getNextFloorId(this.currentFloorId); + } + public boolean hasNextFloor(){ + return player.getServer().getTowerScheduleManager().getNextFloorId(this.currentFloorId) > 0; } public void clearEntry() { this.entryScene = 0; } + + public boolean canEnterScheduleFloor(){ + if(!recordMap.containsKey(player.getServer().getTowerScheduleManager().getLastEntranceFloor())){ + return false; + } + return recordMap.get(player.getServer().getTowerScheduleManager().getLastEntranceFloor()) + .getStarCount() >= 6; + } } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java new file mode 100644 index 000000000..35afbc7ba --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java @@ -0,0 +1,35 @@ +package emu.grasscutter.game.tower; + +import java.util.Date; + +public class TowerScheduleConfig { + private int scheduleId; + + private Date scheduleStartTime; + private Date nextScheduleChangeTime; + + + public int getScheduleId() { + return scheduleId; + } + + public void setScheduleId(int scheduleId) { + this.scheduleId = scheduleId; + } + + public Date getScheduleStartTime() { + return scheduleStartTime; + } + + public void setScheduleStartTime(Date scheduleStartTime) { + this.scheduleStartTime = scheduleStartTime; + } + + public Date getNextScheduleChangeTime() { + return nextScheduleChangeTime; + } + + public void setNextScheduleChangeTime(Date nextScheduleChangeTime) { + this.nextScheduleChangeTime = nextScheduleChangeTime; + } +} diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java new file mode 100644 index 000000000..33f5c158d --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java @@ -0,0 +1,75 @@ +package emu.grasscutter.game.tower; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.TowerScheduleData; +import emu.grasscutter.server.game.GameServer; + +import java.io.FileReader; +import java.util.List; + +public class TowerScheduleManager { + private final GameServer gameServer; + + public GameServer getGameServer() { + return gameServer; + } + + public TowerScheduleManager(GameServer gameServer) { + this.gameServer = gameServer; + this.load(); + } + + private TowerScheduleConfig towerScheduleConfig; + + public synchronized void load(){ + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "TowerSchedule.json")) { + towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class); + + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load tower schedule config.", e); + } + } + + public TowerScheduleConfig getTowerScheduleConfig() { + return towerScheduleConfig; + } + + public TowerScheduleData getCurrentTowerScheduleData(){ + var data = GameData.getTowerScheduleDataMap().get(towerScheduleConfig.getScheduleId()); + if(data == null){ + Grasscutter.getLogger().error("Could not get current tower schedule data by config:{}", towerScheduleConfig); + } + return data; + } + + public List<Integer> getScheduleFloors() { + return getCurrentTowerScheduleData().getSchedules().get(0).getFloorList(); + } + + public int getNextFloorId(int floorId){ + var entranceFloors = getCurrentTowerScheduleData().getEntranceFloorId(); + var nextId = 0; + // find in entrance floors first + for(int i=0;i<entranceFloors.size()-1;i++){ + if(floorId == entranceFloors.get(i)){ + nextId = entranceFloors.get(i+1); + } + } + if(nextId != 0){ + return nextId; + } + var scheduleFloors = getScheduleFloors(); + // find in schedule floors + for(int i=0;i<scheduleFloors.size()-1;i++){ + if(floorId == scheduleFloors.get(i)){ + nextId = scheduleFloors.get(i+1); + } + } + return nextId; + } + + public Integer getLastEntranceFloor() { + return getCurrentTowerScheduleData().getEntranceFloorId().get(getCurrentTowerScheduleData().getEntranceFloorId().size()-1); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index b8ba800a6..611a9aa17 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -90,6 +90,10 @@ public class SceneScriptManager { return config; } + public SceneGroup getCurrentGroup() { + return currentGroup; + } + public List<SceneBlock> getBlocks() { return blocks; } @@ -237,16 +241,16 @@ public class SceneScriptManager { for (SceneSuite suite : group.suites) { suite.sceneMonsters = new ArrayList<>(suite.monsters.size()); - for (int id : suite.monsters) { - try { - SceneMonster monster = (SceneMonster) map.get(id); - if (monster != null) { - suite.sceneMonsters.add(monster); + suite.monsters.forEach(id -> { + Object objEntry = map.get(id.intValue()); + if (objEntry instanceof Map.Entry<?,?> monsterEntry) { + Object monster = monsterEntry.getValue(); + if(monster instanceof SceneMonster sceneMonster){ + suite.sceneMonsters.add(sceneMonster); } - } catch (Exception e) { - continue; } - } + }); + suite.sceneGadgets = new ArrayList<>(suite.gadgets.size()); for (int id : suite.gadgets) { try { @@ -320,13 +324,15 @@ public class SceneScriptManager { } public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { - this.currentGroup = group; - this.monsterSceneLimit = 0; var suite = group.getSuiteByIndex(suiteIndex); if(suite == null){ return; } - suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); + if(suite.sceneMonsters.size() > 0){ + this.currentGroup = group; + this.monsterSceneLimit = 0; + suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); + } } public void spawnMonstersInGroup(SceneGroup group) { @@ -401,6 +407,7 @@ public class SceneScriptManager { spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); }else if(this.monsterAlive.get() == 0){ // spawn the last turn of monsters + //callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs()); while(!this.monsterOrders.isEmpty()){ spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); } diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 1b9badc11..1c4bbd0f2 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -118,7 +118,7 @@ public class ScriptLib { challengeIndex,groupId,ordersConfigId,tideCount,sceneLimit,param6); SceneGroup group = getSceneScriptManager().getGroupById(groupId); - + if (group == null || group.monsters == null) { return 1; } @@ -136,8 +136,7 @@ public class ScriptLib { if (group == null || group.monsters == null) { return 1; } - - // TODO just spawn all from group for now + this.getSceneScriptManager().spawnMonstersInGroup(group, suite); return 0; @@ -159,7 +158,13 @@ public class ScriptLib { if (group == null || group.monsters == null) { return 1; } - + + if(getSceneScriptManager().getScene().getChallenge() != null && + getSceneScriptManager().getScene().getChallenge().inProgress()) + { + return 0; + } + DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), group); challenge.setChallengeId(challengeId); challenge.setChallengeIndex(challengeIndex); @@ -249,7 +254,7 @@ public class ScriptLib { var1); return (int) getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityMonster) + .filter(e -> e instanceof EntityMonster && e.getGroupId() == getSceneScriptManager().getCurrentGroup().id) .count(); } public int SetMonsterBattleByGroup(int var1, int var2, int var3){ @@ -266,13 +271,11 @@ public class ScriptLib { return 0; } // 8-1 - public int GetGroupVariableValueByGroup(int var1, String var2, int var3){ - logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{},{}", - var1,var2,var3); + public int GetGroupVariableValueByGroup(String name, int groupId){ + logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{}", + name,groupId); - //TODO - - return getSceneScriptManager().getVariables().getOrDefault(var2, 0); + return getSceneScriptManager().getVariables().getOrDefault(name, 0); } public int SetIsAllowUseSkill(int canUse, int var2){ @@ -299,4 +302,11 @@ public class ScriptLib { return 0; } + public int SetGroupVariableValueByGroup(String key, int value, int groupId){ + logger.debug("[LUA] Call SetGroupVariableValueByGroup with {},{},{}", + key,value,groupId); + + return 0; + } + } diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 7ce8488ef..cb0e4965d 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -15,6 +15,7 @@ import emu.grasscutter.game.managers.InventoryManager; import emu.grasscutter.game.managers.MultiplayerManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.shop.ShopManager; +import emu.grasscutter.game.tower.TowerScheduleManager; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; @@ -54,6 +55,7 @@ public final class GameServer extends KcpServer { private final DropManager dropManager; private final CombineManger combineManger; + private final TowerScheduleManager towerScheduleManager; public GameServer() { this(new InetSocketAddress( @@ -82,7 +84,7 @@ public final class GameServer extends KcpServer { this.dropManager = new DropManager(this); this.expeditionManager = new ExpeditionManager(this); this.combineManger = new CombineManger(this); - + this.towerScheduleManager = new TowerScheduleManager(this); // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); } @@ -139,6 +141,10 @@ public final class GameServer extends KcpServer { return this.combineManger; } + public TowerScheduleManager getTowerScheduleManager() { + return towerScheduleManager; + } + public TaskMap getTaskMap() { return this.taskMap; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java index 2a9ef2004..38462882f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java @@ -11,7 +11,10 @@ public class HandlerTowerAllDataReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - session.send(new PacketTowerAllDataRsp()); + session.send(new PacketTowerAllDataRsp( + session.getServer().getTowerScheduleManager(), + session.getPlayer().getTowerManager() + )); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java index 479029243..56d844d8d 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java @@ -46,7 +46,7 @@ public class PacketDungeonSettleNotify extends BasePacket { .setCount(1000) .build()) ; - if(nextFloorId > 0){ + if(nextFloorId > 0 && canJump){ towerLevelEndNotify.setNextFloorId(nextFloorId); } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java index d2d2376e6..654aa4a07 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java @@ -1,37 +1,64 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.TowerFloorData; +import emu.grasscutter.game.tower.TowerManager; +import emu.grasscutter.game.tower.TowerScheduleManager; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.TowerAllDataRspOuterClass.TowerAllDataRsp; import emu.grasscutter.net.proto.TowerCurLevelRecordOuterClass.TowerCurLevelRecord; import emu.grasscutter.net.proto.TowerFloorRecordOuterClass.TowerFloorRecord; +import emu.grasscutter.net.proto.TowerLevelRecordOuterClass; +import emu.grasscutter.utils.DateHelper; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; public class PacketTowerAllDataRsp extends BasePacket { - public PacketTowerAllDataRsp() { + public PacketTowerAllDataRsp(TowerScheduleManager towerScheduleManager, TowerManager towerManager) { super(PacketOpcodes.TowerAllDataRsp); - var list = GameData.getTowerFloorDataMap().values().stream() - .map(TowerFloorData::getFloorId) - .map(id -> TowerFloorRecord.newBuilder().setFloorId(id).build()) - .collect(Collectors.toList()); + var recordList = towerManager.getRecordMap().values().stream() + .map(rec -> TowerFloorRecord.newBuilder() + .setFloorId(rec.getFloorId()) + .setFloorStarRewardProgress(rec.getFloorStarRewardProgress()) + .putAllPassedLevelMap(rec.getPassedLevelMap()) + .addAllPassedLevelRecordList(buildFromPassedLevelMap(rec.getPassedLevelMap())) + .build() + ) + .toList(); + + var openTimeMap = towerScheduleManager.getScheduleFloors().stream() + .collect(Collectors.toMap(x -> x, + y -> DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getScheduleStartTime())) + ); TowerAllDataRsp proto = TowerAllDataRsp.newBuilder() - .setTowerScheduleId(29) - .addAllTowerFloorRecordList(list) + .setTowerScheduleId(towerScheduleManager.getCurrentTowerScheduleData().getScheduleId()) + .addAllTowerFloorRecordList(recordList) .setCurLevelRecord(TowerCurLevelRecord.newBuilder().setIsEmpty(true)) - .setNextScheduleChangeTime(Integer.MAX_VALUE) - .putFloorOpenTimeMap(1024, 1630486800) - .putFloorOpenTimeMap(1025, 1630486800) - .putFloorOpenTimeMap(1026, 1630486800) - .putFloorOpenTimeMap(1027, 1630486800) - .setScheduleStartTime(1630486800) + .setScheduleStartTime(DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getScheduleStartTime())) + .setNextScheduleChangeTime(DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getNextScheduleChangeTime())) + .putAllFloorOpenTimeMap(openTimeMap) + .setIsFinishedEntranceFloor(towerManager.canEnterScheduleFloor()) .build(); this.setData(proto); } + + private List<TowerLevelRecordOuterClass.TowerLevelRecord> buildFromPassedLevelMap(Map<Integer, Integer> map){ + return map.entrySet().stream() + .map(item -> TowerLevelRecordOuterClass.TowerLevelRecord.newBuilder() + .setLevelId(item.getKey()) + .addAllSatisfiedCondList(IntStream.range(1, item.getValue() + 1).boxed().toList()) + .build()) + .toList(); + + } + } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java index c0ed414a8..5ab091901 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java @@ -8,13 +8,13 @@ import emu.grasscutter.net.proto.TowerLevelRecordOuterClass.TowerLevelRecord; public class PacketTowerFloorRecordChangeNotify extends BasePacket { - public PacketTowerFloorRecordChangeNotify(int floorId) { + public PacketTowerFloorRecordChangeNotify(int floorId, int stars, boolean canEnterScheduleFloor) { super(PacketOpcodes.TowerFloorRecordChangeNotify); TowerFloorRecordChangeNotify proto = TowerFloorRecordChangeNotify.newBuilder() .addTowerFloorRecordList(TowerFloorRecord.newBuilder() .setFloorId(floorId) - .setFloorStarRewardProgress(3) + .setFloorStarRewardProgress(stars) .addPassedLevelRecordList(TowerLevelRecord.newBuilder() .setLevelId(1) .addSatisfiedCondList(1) @@ -22,7 +22,7 @@ public class PacketTowerFloorRecordChangeNotify extends BasePacket { .addSatisfiedCondList(3) .build()) .build()) - .setIsFinishedEntranceFloor(true) + .setIsFinishedEntranceFloor(canEnterScheduleFloor) .build(); this.setData(proto); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java new file mode 100644 index 000000000..c2c301e4e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java @@ -0,0 +1,32 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerLevelStarCondDataOuterClass.TowerLevelStarCondData; +import emu.grasscutter.net.proto.TowerLevelStarCondNotifyOuterClass.TowerLevelStarCondNotify; + +public class PacketTowerLevelStarCondNotify extends BasePacket { + + public PacketTowerLevelStarCondNotify(int floorId, int levelIndex) { + super(PacketOpcodes.TowerLevelStarCondNotify); + + TowerLevelStarCondNotify proto = TowerLevelStarCondNotify.newBuilder() + .setFloorId(floorId) + .setLevelIndex(levelIndex) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(1) + .build() + ) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(2) + .build() + ) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(3) + .build() + ) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/utils/DateHelper.java b/src/main/java/emu/grasscutter/utils/DateHelper.java index 7005d9457..1f1393760 100644 --- a/src/main/java/emu/grasscutter/utils/DateHelper.java +++ b/src/main/java/emu/grasscutter/utils/DateHelper.java @@ -1,7 +1,7 @@ package emu.grasscutter.utils; -import java.util.Date; import java.util.Calendar; +import java.util.Date; public final class DateHelper { public static Date onlyYearMonthDay(Date now) { @@ -13,4 +13,8 @@ public final class DateHelper { calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } + + public static int getUnixTime(Date localDateTime){ + return (int)(localDateTime.getTime() / 1000L); + } } diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 48ffb5b0b..65a8634f7 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -280,6 +280,9 @@ "invalid_position": "Invalid position.", "success": "Teleported %s to %s, %s, %s in scene %s" }, + "tower": { + "unlock_done": "Abyss Corridor's Floors are all unlocked now." + }, "weather": { "usage": "Usage: weather <weatherId> [climateId]", "success": "Changed weather to %s with climate %s", From 9020ee5b2a2c57ada348a9799a3d94b48b052a7f Mon Sep 17 00:00:00 2001 From: HotaruYS <105128850+HotaruYS@users.noreply.github.com> Date: Sun, 8 May 2022 12:48:06 +0200 Subject: [PATCH 200/434] Override server logging level with environment variable (#653) Use `LOG_LEVEL` environment variable to override logging level for `emu.grasscutter` (which also contains all loggers under it). This might help with debugging various issues reported by users. Previously, the only way to override these levels would be to use `-Dlogback.configurationFile` --- src/main/resources/logback.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1fc6831cb..bd0740fca 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,4 +1,6 @@ <Configuration> + <variable name="LOG_LEVEL" value="${LOG_LEVEL:-INFO}" /> + <appender name="STDOUT" class="emu.grasscutter.utils.JlineLogbackAppender"> <encoder> <pattern>[%d{HH:mm:ss}] [%highlight(%level)] %msg%n</pattern> @@ -14,11 +16,12 @@ <pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n</pattern> </encoder> </appender> - <logger name="org.reflections" level="OFF"/> + + <logger name="org.reflections" level="OFF" /> + <logger name="emu.grasscutter" level="${LOG_LEVEL}" /> + <root level="INFO"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> - <logger name="emu.grasscutter.scripts.ScriptLib" level="DEBUG"> - </logger> </Configuration> \ No newline at end of file From eab964de5cae3fcc174a8f855fbe9787121a517b Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sun, 8 May 2022 01:09:53 -0700 Subject: [PATCH 201/434] Fix: timer is already cancelled. --- .../managers/StaminaManager/StaminaManager.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 5065b12b3..5947880e7 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -29,9 +29,7 @@ public class StaminaManager { private Position previousCoordinates = new Position(0, 0, 0); private MotionState currentState = MotionState.MOTION_STANDBY; private MotionState previousState = MotionState.MOTION_STANDBY; - private final Timer sustainedStaminaHandlerTimer = new Timer(); - private final SustainedStaminaHandler handleSustainedStamina = new SustainedStaminaHandler(); - private boolean timerRunning = false; + private Timer sustainedStaminaHandlerTimer; private GameSession cachedSession = null; private GameEntity cachedEntity = null; private int staminaRecoverDelay = 0; @@ -136,21 +134,21 @@ public class StaminaManager { entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); player.getScene().removeEntity(entity); - ((EntityAvatar)entity).onDeath(dieType, 0); + ((EntityAvatar) entity).onDeath(dieType, 0); } public void startSustainedStaminaHandler() { - if (!player.isPaused() && !timerRunning) { - timerRunning = true; - sustainedStaminaHandlerTimer.scheduleAtFixedRate(handleSustainedStamina, 0, 200); + if (!player.isPaused() && sustainedStaminaHandlerTimer == null) { + sustainedStaminaHandlerTimer = new Timer(); + sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); } } public void stopSustainedStaminaHandler() { - if (timerRunning) { - timerRunning = false; + if (sustainedStaminaHandlerTimer != null) { sustainedStaminaHandlerTimer.cancel(); + sustainedStaminaHandlerTimer = null; // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); } } From ccdfd15bb8e93c91ae93918db1df2365eafb1ba0 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Sun, 8 May 2022 04:02:45 -0700 Subject: [PATCH 202/434] Update StaminaManager --- .../AfterUpdateStaminaListener.java | 12 + .../BeforeUpdateStaminaListener.java | 20 ++ .../StaminaManager/ConsumptionType.java | 8 +- .../game/managers/StaminaManager/README.md | 73 +++++ .../StaminaManager/StaminaManager.java | 265 +++++++++++++----- .../recv/HandlerCombatInvocationsNotify.java | 44 +-- 6 files changed, 327 insertions(+), 95 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java new file mode 100644 index 000000000..bb4f0b188 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java @@ -0,0 +1,12 @@ +package emu.grasscutter.game.managers.StaminaManager; + +public interface AfterUpdateStaminaListener { + /** + * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina. + * This gives listeners a chance to intercept this update. + * + * @param reason Why updating stamina. + * @param newStamina New Stamina value. + */ + void onAfterUpdateStamina(String reason, int newStamina); +} diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java new file mode 100644 index 000000000..02f1f3522 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java @@ -0,0 +1,20 @@ +package emu.grasscutter.game.managers.StaminaManager; + +public interface BeforeUpdateStaminaListener { + /** + * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina. + * This gives listeners a chance to intercept this update. + * @param reason Why updating stamina. + * @param newStamina New ABSOLUTE stamina value. + * @return true if you want to cancel this update, otherwise false. + */ + int onBeforeUpdateStamina(String reason, int newStamina); + /** + * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina. + * This gives listeners a chance to intercept this update. + * @param reason Why updating stamina. + * @param consumption ConsumptionType and RELATIVE stamina change amount. + * @return true if you want to cancel this update, otherwise false. + */ + Consumption onBeforeUpdateStamina(String reason, Consumption consumption); +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java index 9a2d8ae24..9afb2171c 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java @@ -10,10 +10,10 @@ public enum ConsumptionType { SPRINT(-1800), DASH(-360), FLY(-60), - SWIM_DASH_START(-200), - SWIM_DASH(-200), - SWIMMING(-80), - FIGHT(0), + SWIM_DASH_START(-20), + SWIM_DASH(-204), + SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height. + FIGHT(0), // See StaminaManager.getFightConsumption() // restore STANDBY(500), diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md b/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md new file mode 100644 index 000000000..39a4e7988 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md @@ -0,0 +1,73 @@ +# Stamina Manager + +--- +## UpdateStamina +```java +// will use consumption.consumptionType as reason +public int updateStaminaRelative(GameSession session, Consumption consumption); +``` +```java +public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) +``` + +--- +## Pause and Resume +```java +public void startSustainedStaminaHandler() +``` +```java +public void stopSustainedStaminaHandler() +``` + + +--- +## Stamina change listeners and intercepting +### BeforeUpdateStaminaListener +```java + +import emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener; + +// Listener sample: plugin disable CLIMB_JUMP stamina cost. +private class MyClass implements BeforeUpdateStaminaListener { + // Make your class implement the listener, and pass in your class as a listener. + + public MyClass() { + getStaminaManager().registerBeforeUpdateStaminaListener("myClass", this); + } + + @Override + public boolean onBeforeUpdateStamina(String reason, int newStamina) { + // do not intercept this update + return false; + } + + @Override + public boolean onBeforeUpdateStamina(String reason, Consumption consumption) { + // Try to intercept if this update is CLIMB_JUMP + if (consumption.consumptionType == ConsumptionType.CLIMB_JUMP) { + return true; + } + // If it is not CLIMB_JUMP, do not intercept. + return false; + } +} +``` +### AfterUpdateStaminaListener +```java + +import emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener; + +// Listener sample: plugin listens for changes already made. +private class MyClass implements AfterUpdateStaminaListener { + // Make your class implement the listener, and pass in your class as a listener. + + public MyClass() { + registerAfterUpdateStaminaListener("myClass", this); + } + + @Override + public void onAfterUpdateStamina(String reason, int newStamina) { + // ... + } +} +``` \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 5947880e7..72b91c055 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -33,94 +33,170 @@ public class StaminaManager { private GameSession cachedSession = null; private GameEntity cachedEntity = null; private int staminaRecoverDelay = 0; - private boolean isInSkillMove = false; - public boolean getIsInSkillMove() { - return isInSkillMove; - } - public void setIsInSkillMove(boolean b) { - isInSkillMove = b; - } + + private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>(); + private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>(); public StaminaManager(Player player) { this.player = player; MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( - MotionState.MOTION_SWIM_MOVE, - MotionState.MOTION_SWIM_IDLE, - MotionState.MOTION_SWIM_DASH, - MotionState.MOTION_SWIM_JUMP + MotionState.MOTION_SWIM_MOVE, + MotionState.MOTION_SWIM_IDLE, + MotionState.MOTION_SWIM_DASH, + MotionState.MOTION_SWIM_JUMP ))); MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList( - MotionState.MOTION_STANDBY, - MotionState.MOTION_STANDBY_MOVE, - MotionState.MOTION_DANGER_STANDBY, - MotionState.MOTION_DANGER_STANDBY_MOVE, - MotionState.MOTION_LADDER_TO_STANDBY, - MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY + MotionState.MOTION_STANDBY, + MotionState.MOTION_STANDBY_MOVE, + MotionState.MOTION_DANGER_STANDBY, + MotionState.MOTION_DANGER_STANDBY_MOVE, + MotionState.MOTION_LADDER_TO_STANDBY, + MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY ))); MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList( - MotionState.MOTION_CLIMB, - MotionState.MOTION_CLIMB_JUMP, - MotionState.MOTION_STANDBY_TO_CLIMB, - MotionState.MOTION_LADDER_IDLE, - MotionState.MOTION_LADDER_MOVE, - MotionState.MOTION_LADDER_SLIP, - MotionState.MOTION_STANDBY_TO_LADDER + MotionState.MOTION_CLIMB, + MotionState.MOTION_CLIMB_JUMP, + MotionState.MOTION_STANDBY_TO_CLIMB, + MotionState.MOTION_LADDER_IDLE, + MotionState.MOTION_LADDER_MOVE, + MotionState.MOTION_LADDER_SLIP, + MotionState.MOTION_STANDBY_TO_LADDER ))); MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList( - MotionState.MOTION_FLY, - MotionState.MOTION_FLY_IDLE, - MotionState.MOTION_FLY_SLOW, - MotionState.MOTION_FLY_FAST, - MotionState.MOTION_POWERED_FLY + MotionState.MOTION_FLY, + MotionState.MOTION_FLY_IDLE, + MotionState.MOTION_FLY_SLOW, + MotionState.MOTION_FLY_FAST, + MotionState.MOTION_POWERED_FLY ))); MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList( - MotionState.MOTION_DASH, - MotionState.MOTION_DANGER_DASH, - MotionState.MOTION_DASH_BEFORE_SHAKE, - MotionState.MOTION_RUN, - MotionState.MOTION_DANGER_RUN, - MotionState.MOTION_WALK, - MotionState.MOTION_DANGER_WALK + MotionState.MOTION_DASH, + MotionState.MOTION_DANGER_DASH, + MotionState.MOTION_DASH_BEFORE_SHAKE, + MotionState.MOTION_RUN, + MotionState.MOTION_DANGER_RUN, + MotionState.MOTION_WALK, + MotionState.MOTION_DANGER_WALK ))); MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( - MotionState.MOTION_FIGHT + MotionState.MOTION_FIGHT ))); + + MotionStatesCategorized.put("SKIFF", new HashSet<>(Arrays.asList( + MotionState.MOTION_SKIFF_BOARDING, + MotionState.MOTION_SKIFF_NORMAL, + MotionState.MOTION_SKIFF_DASH, + MotionState.MOTION_SKIFF_POWERED_DASH + ))); + } + + // Listeners + + public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) { + if (beforeUpdateStaminaListeners.containsKey(listenerName)) { + return false; + } + beforeUpdateStaminaListeners.put(listenerName, listener); + return true; + } + + public boolean unregisterBeforeUpdateStaminaListener(String listenerName) { + if (!beforeUpdateStaminaListeners.containsKey(listenerName)) { + return false; + } + beforeUpdateStaminaListeners.remove(listenerName); + return true; + } + + public boolean registerAfterUpdateStaminaListener(String listenerName, AfterUpdateStaminaListener listener) { + if (afterUpdateStaminaListeners.containsKey(listenerName)) { + return false; + } + afterUpdateStaminaListeners.put(listenerName, listener); + return true; + } + + public boolean unregisterAfterUpdateStaminaListener(String listenerName) { + if (!afterUpdateStaminaListeners.containsKey(listenerName)) { + return false; + } + afterUpdateStaminaListeners.remove(listenerName); + return true; } private boolean isPlayerMoving() { float diffX = currentCoordinates.getX() - previousCoordinates.getX(); float diffY = currentCoordinates.getY() - previousCoordinates.getY(); float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); - Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + + Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; } - // Returns new stamina and sends PlayerPropNotify - public int updateStamina(GameSession session, Consumption consumption) { + public int updateStaminaRelative(GameSession session, Consumption consumption) { int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); if (consumption.amount == 0) { return currentStamina; } + // notify will update + for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { + Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.consumptionType.toString(), consumption); + if ((overriddenConsumption.consumptionType != consumption.consumptionType) && (overriddenConsumption.amount != consumption.amount)) { + Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" + + consumption.consumptionType.toString() + ", " + consumption.amount + ") overridden to relative(" + + consumption.consumptionType.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); + return currentStamina; + } + } int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); - Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + + Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); int newStamina = currentStamina + consumption.amount; if (newStamina < 0) { newStamina = 0; - } - if (newStamina > playerMaxStamina) { + } else if (newStamina > playerMaxStamina) { newStamina = playerMaxStamina; } + return setStamina(session, consumption.consumptionType.toString(), newStamina); + } + + public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) { + int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + // notify will update + for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { + int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina); + if (overriddenNewStamina != newStamina) { + Grasscutter.getLogger().debug("[StaminaManager] Stamina update absolute(" + + reason + ", " + newStamina + ") overridden to absolute(" + + reason + ", " + newStamina + ") by: " + listener.getKey()); + return currentStamina; + } + } + int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); + if (newStamina < 0) { + newStamina = 0; + } else if (newStamina > playerMaxStamina) { + newStamina = playerMaxStamina; + } + return setStamina(session, reason, newStamina); + } + + // Returns new stamina and sends PlayerPropNotify + public int setStamina(GameSession session, String reason, int newStamina) { + // set stamina player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); + // notify updated + for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { + listener.getValue().onAfterUpdateStamina(reason, newStamina); + } return newStamina; } @@ -141,7 +217,7 @@ public class StaminaManager { if (!player.isPaused() && sustainedStaminaHandlerTimer == null) { sustainedStaminaHandlerTimer = new Timer(); sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); - // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); + Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); } } @@ -149,7 +225,7 @@ public class StaminaManager { if (sustainedStaminaHandlerTimer != null) { sustainedStaminaHandlerTimer.cancel(); sustainedStaminaHandlerTimer = null; - // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); + Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); } } @@ -188,17 +264,17 @@ public class StaminaManager { switch (motionState) { case MOTION_DASH_BEFORE_SHAKE: if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { - updateStamina(session, new Consumption(ConsumptionType.SPRINT)); + updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT)); } break; case MOTION_CLIMB_JUMP: if (previousState != MotionState.MOTION_CLIMB_JUMP) { - updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP)); + updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP)); } break; case MOTION_SWIM_DASH: if (previousState != MotionState.MOTION_SWIM_DASH) { - updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START)); + updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START)); } break; } @@ -206,7 +282,7 @@ public class StaminaManager { private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) { Consumption consumption = getFightConsumption(notify.getSkillId()); - updateStamina(session, consumption); + updateStaminaRelative(session, consumption); } private class SustainedStaminaHandler extends TimerTask { @@ -216,22 +292,30 @@ public class StaminaManager { int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); if (moving || (currentStamina < maxStamina)) { - Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + + Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " + (currentStamina >= maxStamina) + ", recalculate stamina"); + Consumption consumption = new Consumption(ConsumptionType.None); - if (!isInSkillMove) { - if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - consumption = getClimbSustainedConsumption(); - } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { - consumption = getSwimSustainedConsumptions(); - } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - consumption = getRunWalkDashSustainedConsumption(); - } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - consumption = getFlySustainedConsumption(); - } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - consumption = getStandSustainedConsumption(); - } + if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { + consumption = getClimbSustainedConsumption(); + } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { + consumption = getSwimSustainedConsumptions(); + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + consumption = getRunWalkDashSustainedConsumption(); + } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { + consumption = getFlySustainedConsumption(); + } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { + consumption = getStandSustainedConsumption(); } + + /* + TODO: Reductions that apply to all motion types: + Elemental Resonance + Wind: -15% + Skills + Diona E: -10% while shield lasts + Barbara E: -12% while lasts + */ if (cachedSession != null) { if (consumption.amount < 0) { staminaRecoverDelay = 0; @@ -241,12 +325,12 @@ public class StaminaManager { if (staminaRecoverDelay < 10) { // For others recover after 2 seconds (10 ticks) - as official server does. staminaRecoverDelay++; - consumption = new Consumption(ConsumptionType.None); + consumption.amount = 0; + Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); } } - updateStamina(cachedSession, consumption); + updateStaminaRelative(cachedSession, consumption); } - handleDrowning(); } } previousState = currentState; @@ -261,10 +345,9 @@ public class StaminaManager { private void handleDrowning() { int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); if (stamina < 10) { - boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState); - Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + - player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming); - if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) { + Grasscutter.getLogger().trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState); + if (currentState != MotionState.MOTION_SWIM_IDLE) { killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); } } @@ -272,7 +355,33 @@ public class StaminaManager { // Consumption Calculators + // Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina + private Consumption getFightConsumption(int skillCasting) { + /* TODO: + Instead of handling here, consider call StaminaManager.updateStamina****() with a Consumption object with + type=FIGHT and a modified amount when handling attacks for more accurate attack start/end time and + other info. Handling it here could be very complicated. + Charged attack + Default: + Polearm: (-2500) + Claymore: (-4000 per second, -800 each tick) + Catalyst: (-5000) + Talent: + Ningguang: When Ningguang is in possession of Star Jades, her Charged Attack does not consume Stamina. (Catalyst * 0) + Klee: When Jumpy Dumpty and Normal Attacks deal DMG, Klee has a 50% chance to obtain an Explosive Spark. + This Explosive Spark is consumed by the next Charged Attack, which costs no Stamina. (Catalyst * 0) + Constellations: + Hu Tao: While in a Paramita Papilio state activated by Guide to Afterlife, Hu Tao's Charge Attacks do not consume Stamina. (Polearm * 0) + Character Specific: + Keqing: (-2500) + Diluc: (Claymore * 0.5) + Talent Moving: (Those are skills too) + Ayaka: (-1000 initial) (-1500 per second) When the Cryo application at the end of Kamisato Art: Senho hits an opponent (+1000) + Mona: (-1000 initial) (-1500 per second) + */ + + // TODO: Currently only handling Ayaka and Mona's talent moving initial costs. Consumption consumption = new Consumption(ConsumptionType.None); HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ put(10013, -1000); // Kamisato Ayaka @@ -292,10 +401,12 @@ public class StaminaManager { consumption = new Consumption(ConsumptionType.CLIMB_START); } } + // TODO: Foods return consumption; } private Consumption getSwimSustainedConsumptions() { + handleDrowning(); Consumption consumption = new Consumption(ConsumptionType.None); if (currentState == MotionState.MOTION_SWIM_MOVE) { consumption = new Consumption(ConsumptionType.SWIMMING); @@ -310,6 +421,7 @@ public class StaminaManager { Consumption consumption = new Consumption(ConsumptionType.None); if (currentState == MotionState.MOTION_DASH) { consumption = new Consumption(ConsumptionType.DASH); + // TODO: Foods } if (currentState == MotionState.MOTION_RUN) { consumption = new Consumption(ConsumptionType.RUN); @@ -321,7 +433,12 @@ public class StaminaManager { } private Consumption getFlySustainedConsumption() { + // POWERED_FLY, e.g. wind tunnel + if (currentState == MotionState.MOTION_POWERED_FLY) { + return new Consumption(ConsumptionType.POWERED_FLY); + } Consumption consumption = new Consumption(ConsumptionType.FLY); + // Talent HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ put(212301, 0.8f); // Amber put(222301, 0.8f); // Venti @@ -330,15 +447,15 @@ public class StaminaManager { for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) { for (int skillId : entity.getAvatar().getProudSkillList()) { if (glidingCostReduction.containsKey(skillId)) { - reduction = glidingCostReduction.get(skillId); + float potentialLowerReduction = glidingCostReduction.get(skillId); + if (potentialLowerReduction < reduction) { + reduction = potentialLowerReduction; + } } } } consumption.amount *= reduction; - // POWERED_FLY, e.g. wind tunnel - if (currentState == MotionState.MOTION_POWERED_FLY) { - consumption = new Consumption(ConsumptionType.POWERED_FLY); - } + // TODO: Foods return consumption; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index cc9e7b345..36252f828 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity); - // TODO: handle MOTION_FIGHT landing - // For plunge attacks, LAND_SPEED is always -30 and is not useful. - // May need the height when starting plunge attack. + // TODO: handle MOTION_FIGHT landing which has a different damage factor + // Also, for plunge attacks, LAND_SPEED is always -30 and is not useful. + // May need the height when starting plunge attack. + // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets. + // Cache land speed for later use. + if (motionState == MotionState.MOTION_LAND_SPEED) { + cachedLandingSpeed = motionInfo.getSpeed().getY(); + cachedLandingTimeMillisecond = System.currentTimeMillis(); + monitorLandingEvent = true; + } if (monitorLandingEvent) { if (motionState == MotionState.MOTION_FALL_ON_GROUND) { monitorLandingEvent = false; handleFallOnGround(session, entity, motionState); } } - if (motionState == MotionState.MOTION_LAND_SPEED) { - // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packet. Cache land speed for later use. - cachedLandingSpeed = motionInfo.getSpeed().getY(); - cachedLandingTimeMillisecond = System.currentTimeMillis(); - monitorLandingEvent = true; - } } break; default: @@ -84,33 +85,42 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { } private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { - // If not received immediately after MOTION_LAND_SPEED, discard this packet. + // People have reported that after plunge attack (client sends a FIGHT instead of FALL_ON_GROUND) they will die + // if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping again. + // A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet. + // 200ms seems to be a reasonable delay. int maxDelay = 200; long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond; - Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : "")); + Grasscutter.getLogger().trace("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : "")); if (actualDelay > maxDelay) { return; } float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float damage = 0; + float damageFactor = 0; if (cachedLandingSpeed < -23.5) { - damage = (float) (maxHP * 0.33); + damageFactor = 0.33f; } if (cachedLandingSpeed < -25) { - damage = (float) (maxHP * 0.5); + damageFactor = 0.5f; } if (cachedLandingSpeed < -26.5) { - damage = (float) (maxHP * 0.66); + damageFactor = 0.66f; } if (cachedLandingSpeed < -28) { - damage = (maxHP * 1); + damageFactor = 1f; } + float damage = maxHP * damageFactor; float newHP = currentHP - damage; if (newHP < 0) { newHP = 0; } - Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP); + if (damageFactor > 0) { + Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\tLandingSpeed: " + cachedLandingSpeed + + "\tDamageFactor: " + damageFactor + "\tDamage: " + damage + "\tNewHP: " + newHP); + } else { + Grasscutter.getLogger().trace(currentHP + "/" + maxHP + "\tLandingSpeed: 0\tNo damage"); + } entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); if (newHP == 0) { From 55b692561fb2a734b1965eb4095c705bc74a4077 Mon Sep 17 00:00:00 2001 From: HotaruYS <105128850+HotaruYS@users.noreply.github.com> Date: Sun, 8 May 2022 14:30:48 +0200 Subject: [PATCH 203/434] Improve logging pattern by including caller class name --- src/main/resources/logback.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index bd0740fca..656c1b443 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -3,7 +3,7 @@ <appender name="STDOUT" class="emu.grasscutter.utils.JlineLogbackAppender"> <encoder> - <pattern>[%d{HH:mm:ss}] [%highlight(%level)] %msg%n</pattern> + <pattern>%d{HH:mm:ss} <%highlight(%level):%gray(%class{0})> %msg%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> @@ -13,7 +13,7 @@ <maxHistory>24</maxHistory> </rollingPolicy> <encoder> - <pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n</pattern> + <pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} <%level:%class> %m%n</pattern> </encoder> </appender> @@ -24,4 +24,4 @@ <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> -</Configuration> \ No newline at end of file +</Configuration> From 342cf336618431a699ac3fac0cc89d68f8243de6 Mon Sep 17 00:00:00 2001 From: Michaellan <67815438+chrisblue@users.noreply.github.com> Date: Sun, 8 May 2022 20:06:10 +0800 Subject: [PATCH 204/434] fill description --- src/main/resources/languages/zh-CN.json | 105 ++++++++++++++++-------- 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 8faa0e4ae..4e4929aee 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -95,17 +95,20 @@ "create": "已建立账号,UID 为 %s 。", "delete": "账号已刪除。", "no_account": "账号不存在。", - "command_usage": "用法:account <create|delete> <username> [uid]" + "command_usage": "用法:account <create|delete> <username> [uid]", + "description": "创建或删除账号。" }, "broadcast": { "command_usage": "用法:broadcast <消息>", - "message_sent": "公告已发送。" + "message_sent": "公告已发送。", + "description": "向所有玩家发送公告。" }, "changescene": { "usage": "用法:changescene <scene id>", "already_in_scene": "你已经在这个秘境中了。", "success": "已切换至秘境 %s.", - "exists_error": "此秘境不存在。" + "exists_error": "此秘境不存在。", + "description": "切换指定秘境。" }, "clear": { "command_usage": "用法: clear <all|wp|art|mat>", @@ -115,35 +118,41 @@ "furniture": "已将 %s 的尘歌壶家具清空。", "displays": "已清除 %s 的显示。", "virtuals": "已将 %s 的所有货币和经验值清空。", - "everything": "已将 %s 的所有物品清空。" + "everything": "已将 %s 的所有物品清空。", + "description": "从您的背包中删除所有未装备且已解锁的物品,包括稀有物品。" }, "coop": { "usage": "用法:coop <playerId> <target playerId>", - "success": "已强制召唤 %s 到 %s的世界" + "success": "已强制召唤 %s 到 %s的世界", + "description": "强制召唤指定用户到他人的世界。" }, "enter_dungeon": { "usage": "用法:enterdungeon <dungeon id>", "changed": "已进入秘境 %s", "not_found_error": "此秘境不存在。", - "in_dungeon_error": "你已经在秘境中了。" + "in_dungeon_error": "你已经在秘境中了。", + "description": "进入指定秘境。" }, "giveAll": { "usage": "用法:giveall [player] [amount]", "started": "正在给予全部物品...", "success": "已给予全部物品。", - "invalid_amount_or_playerId": "无效的数量/玩家ID。" + "invalid_amount_or_playerId": "无效的数量/玩家ID。", + "description": "给予所有物品。" }, "giveArtifact": { "usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", "id_error": "无效的圣遗物ID。", - "success": "已将 %s 给予 %s。" + "success": "已将 %s 给予 %s。", + "description": "给予指定圣遗物。" }, "giveChar": { "usage": "用法:givechar <player> <itemId|itemName> [amount]", "given": "给予角色 %s 等级 %s 向UID %s.", "invalid_avatar_id": "无效的角色ID。", "invalid_avatar_level": "无效的角色等級。.", - "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。" + "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。", + "description": "给予指定角色。" }, "give": { "usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]", @@ -151,29 +160,36 @@ "refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。", "given": "已将 %s 个 %s 给予 %s。", "given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s", - "given_level": "已将 %s 等级 %s %s 个给予UID %s" + "given_level": "已将 %s 等级 %s %s 个给予UID %s", + "description": "给予指定物品。" }, "godmode": { - "success": "上帝模式已被设置为 %s 。 [用户:%s]" + "success": "上帝模式已被设置为 %s 。 [用户:%s]", + "description": "防止你受到伤害。" }, "heal": { - "success": "所有角色已被治疗。" + "success": "所有角色已被治疗。", + "description": "治疗所选队伍的角色。" }, "kick": { "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", - "server_kick_player": "正在踢出玩家 [%s:%s]" + "server_kick_player": "正在踢出玩家 [%s:%s]", + "description": "从服务器内踢出指定玩家。" }, "kill": { "usage": "用法:killall [playerUid] [sceneId]", "scene_not_found_in_player_world": "未在玩家世界中找到此场景", - "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]" + "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]", + "description": "杀死所有怪物" }, "killCharacter": { "usage": "用法:/killcharacter [playerId]", - "success": "已杀死 %s 目前使用的角色。" + "success": "已杀死 %s 目前使用的角色。", + "description": "杀死目前使用的角色" }, "list": { - "success": "目前在线人数:%s" + "success": "目前在线人数:%s", + "description": "查看所有玩家" }, "permission": { "usage": "用法:permission <add|remove> <username> <permission>", @@ -181,21 +197,26 @@ "has_error": "此玩家已拥有此权限!", "remove": "权限已移除。", "not_have_error": "此玩家未拥有权限!", - "account_error": "账号不存在!" + "account_error": "账号不存在!", + "description": "给予或移除指定玩家的权限。" }, "position": { - "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d" + "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d", + "description": "获取所在位置。" }, "reload": { "reload_start": "正在重载配置文件和数据。", - "reload_done": "重载完毕。" + "reload_done": "重载完毕。", + "description": "重载配置文件和数据。" }, "resetConst": { "reset_all": "重置所有角色的命座。", - "success": "已重置 %s 的命座,重新登录后将会生效。" + "success": "已重置 %s 的命座,重新登录后将会生效。", + "description": "重置当前角色的命之座,执行命令后需重新登录以生效。" }, "resetShopLimit": { - "usage": "用法:/resetshop <player id>" + "usage": "用法:/resetshop <player id>", + "description": "重置所选玩家的商店刷新时间。" }, "sendMail": { "usage": "用法:give [player] <itemId|itemName> [amount]", @@ -217,17 +238,20 @@ "message": "<正文>", "sender": "<发件人>", "arguments": "<itemId|itemName|finish> [数量] [等级]", - "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。" + "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。", + "description": "向指定用户发送邮件。 此命令的用法可根据附加的参数而变化。" }, "sendMessage": { "usage": "用法:sendmessage <player> <message>", - "success": "消息已发送。" + "success": "消息已发送。", + "description": "向指定玩家发送消息" }, "setFetterLevel": { "usage": "用法:setfetterlevel <level>", "range_error": "好感度等级必须在 0 到 10 之间。", "fetter_set_level": "好感度已设置为 %s 级", - "level_error": "无效的好感度等级。" + "level_error": "无效的好感度等级。", + "description": "设置当前角色的好感度等级。" }, "setStats": { "usage_console": "用法:setstats|stats @<UID> <stat> <value>", @@ -238,20 +262,24 @@ "player_error": "玩家不存在或已离线。", "set_self": "%s 已经设置为 %s。", "set_for_uid": "%s 的使用者 %s 更改为 %s。", - "set_max_hp": "最大生命值更改为 %s。" + "set_max_hp": "最大生命值更改为 %s。", + "description": "设置当前角色的属性。" }, "setWorldLevel": { "usage": "用法:setworldlevel <level>", "value_error": "世界等级必须设置在0-8之间。", "success": "已将世界等级设为%s。", - "invalid_world_level": "无效的世界等级。" + "invalid_world_level": "无效的世界等级。", + "description": "设置世界等级,执行命令后需重新登录以生效。" }, "spawn": { "usage": "用法:spawn <entityId> [amount] [level(仅限怪物]", - "success": "已生成 %s 个 %s。" + "success": "已生成 %s 个 %s。", + "description": "在你附近生成一个生物。" }, "stop": { - "success": "正在关闭服务器..." + "success": "正在关闭服务器...", + "description": "停止服务器" }, "talent": { "usage_1": "设置天赋等级:/talent set <talentID> <value>", @@ -267,32 +295,41 @@ "invalid_level": "无效的天赋等级。", "normal_attack_id": "普通攻击的 ID 为 %s。", "e_skill_id": "元素战技ID %s。", - "q_skill_id": "元素爆发ID %s。" + "q_skill_id": "元素爆发ID %s。", + "description": "设置当前角色的天赋等级。" }, "teleportAll": { "success": "已将全部玩家传送到你的位置", - "error": "命令仅限处于多人游戏状态下使用。" + "error": "命令仅限处于多人游戏状态下使用。", + "description": "将你世界中的所有玩家传送到你所在的位置。" }, "teleport": { "usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]", "usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]", "specify_player_id": "你必须指定一个玩家ID。", "invalid_position": "无效的位置。", - "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s" + "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s", + "description": "改变指定玩家的位置。" }, "weather": { "usage": "用法:weather <weatherId> [climateId]", "success": "已将当前天气设定为 %s,气候为 %s。", - "invalid_id": "无效的天气ID。" + "invalid_id": "无效的天气ID。", + "description": "改变天气" }, "drop": { "command_usage": "用法:drop <itemId|itemName> [amount]", - "success": "已将 %s x %s 丟在附近。" + "success": "已将 %s x %s 丟在附近。", + "description": "在你附近丢一个物品。" }, "help": { "usage": "用法:", "aliases": "別名:", - "available_commands": "可用指令:" + "available_commands": "可用指令:", + "description": "发送帮助信息或显示指定命令的信息。" + }, + "restart": { + "description": "重新启动服务器。" } } } From 6d1ef0d841de6b1cf095197ad217762226de0c7f Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sun, 8 May 2022 04:40:01 -0700 Subject: [PATCH 205/434] Implement AbilityManager --- proto/AbilityActionGenerateElemBall.proto | 11 ++ proto/AbilityMetaModifierChange.proto | 21 ++ proto/AbilityMetaReInitOverrideMap.proto | 9 + proto/ModifierAction.proto | 8 + proto/ModifierProperty.proto | 10 + .../java/emu/grasscutter/data/GameData.java | 7 + .../emu/grasscutter/data/ResourceLoader.java | 69 +++++++ .../data/custom/AbilityModifier.java | 36 ++++ .../data/custom/AbilityModifierEntry.java | 37 ++++ .../game/ability/AbilityManager.java | 185 ++++++++++++++++++ .../grasscutter/game/entity/EntityAvatar.java | 17 ++ .../grasscutter/game/entity/GameEntity.java | 73 +++++++ .../emu/grasscutter/game/player/Player.java | 9 +- .../emu/grasscutter/game/world/Scene.java | 22 +-- .../recv/HandlerAbilityInvocationsNotify.java | 3 +- .../HandlerClientAbilityInitFinishNotify.java | 2 + .../recv/HandlerEvtCreateGadgetNotify.java | 5 - .../recv/HandlerEvtDestroyGadgetNotify.java | 5 - .../HandlerSetEntityClientDataNotify.java | 2 +- 19 files changed, 497 insertions(+), 34 deletions(-) create mode 100644 proto/AbilityActionGenerateElemBall.proto create mode 100644 proto/AbilityMetaModifierChange.proto create mode 100644 proto/AbilityMetaReInitOverrideMap.proto create mode 100644 proto/ModifierAction.proto create mode 100644 proto/ModifierProperty.proto create mode 100644 src/main/java/emu/grasscutter/data/custom/AbilityModifier.java create mode 100644 src/main/java/emu/grasscutter/data/custom/AbilityModifierEntry.java create mode 100644 src/main/java/emu/grasscutter/game/ability/AbilityManager.java diff --git a/proto/AbilityActionGenerateElemBall.proto b/proto/AbilityActionGenerateElemBall.proto new file mode 100644 index 000000000..d048d75cd --- /dev/null +++ b/proto/AbilityActionGenerateElemBall.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "Vector.proto"; + +message AbilityActionGenerateElemBall { + Vector pos = 1; + Vector rot = 2; + uint32 room_id = 3; +} diff --git a/proto/AbilityMetaModifierChange.proto b/proto/AbilityMetaModifierChange.proto new file mode 100644 index 000000000..43d87a819 --- /dev/null +++ b/proto/AbilityMetaModifierChange.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "ModifierAction.proto"; +import "AbilityString.proto"; +import "AbilityAttachedModifier.proto"; +import "ModifierProperty.proto"; + +message AbilityMetaModifierChange { + ModifierAction action = 1; + AbilityString parent_ability_name = 2; + AbilityString parent_ability_override = 3; + AbilityAttachedModifier attached_instanced_modifier = 4; + repeated ModifierProperty properties = 5; + int32 modifier_local_id = 6; + bool is_mute_remote = 7; + uint32 apply_entity_id = 8; + bool is_attached_parent_ability = 9; + uint32 server_buff_uid = 10; +} diff --git a/proto/AbilityMetaReInitOverrideMap.proto b/proto/AbilityMetaReInitOverrideMap.proto new file mode 100644 index 000000000..3a8554004 --- /dev/null +++ b/proto/AbilityMetaReInitOverrideMap.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "AbilityScalarValueEntry.proto"; + +message AbilityMetaReInitOverrideMap { + repeated AbilityScalarValueEntry override_map = 1; +} diff --git a/proto/ModifierAction.proto b/proto/ModifierAction.proto new file mode 100644 index 000000000..a7a1c37a7 --- /dev/null +++ b/proto/ModifierAction.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +enum ModifierAction { + ADDED = 0; + REMOVED = 1; +} diff --git a/proto/ModifierProperty.proto b/proto/ModifierProperty.proto new file mode 100644 index 000000000..cc0aa742c --- /dev/null +++ b/proto/ModifierProperty.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "AbilityString.proto"; + +message ModifierProperty { + AbilityString key = 1; + float value = 2; +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 76a7f1652..f06d1bda6 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -9,6 +9,8 @@ import java.util.Map; import emu.grasscutter.Grasscutter; import emu.grasscutter.utils.Utils; import emu.grasscutter.data.custom.AbilityEmbryoEntry; +import emu.grasscutter.data.custom.AbilityModifier; +import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.*; @@ -22,6 +24,7 @@ public class GameData { // BinOutputs private static final Int2ObjectMap<String> abilityHashes = new Int2ObjectOpenHashMap<>(); private static final Map<String, AbilityEmbryoEntry> abilityEmbryos = new HashMap<>(); + private static final Map<String, AbilityModifierEntry> abilityModifiers = new HashMap<>(); private static final Map<String, OpenConfigEntry> openConfigEntries = new HashMap<>(); private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>(); @@ -101,6 +104,10 @@ public class GameData { return abilityEmbryos; } + public static Map<String, AbilityModifierEntry> getAbilityModifiers() { + return abilityModifiers; + } + public static Map<String, OpenConfigEntry> getOpenConfigEntries() { return openConfigEntries; } diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index b1e3da9ff..c2708bd63 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -18,6 +18,11 @@ 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.AbilityModifier; +import emu.grasscutter.data.custom.AbilityModifier.AbilityConfigData; +import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; +import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.game.world.SpawnDataEntry; @@ -47,6 +52,7 @@ public class ResourceLoader { // Load ability lists loadAbilityEmbryos(); loadOpenConfig(); + loadAbilityModifiers(); // Load resources loadResources(); // Process into depots @@ -244,6 +250,69 @@ public class ResourceLoader { } } + private static void loadAbilityModifiers() { + // Load from BinOutput + File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Ability/Temp/AvatarAbilities/")); + File[] files = folder.listFiles(); + if (files == null) { + Grasscutter.getLogger().error("Error loading ability modifiers: no files found in " + folder.getAbsolutePath()); + return; + } + + for (File file : files) { + List<AbilityConfigData> abilityConfigList = null; + + try (FileReader fileReader = new FileReader(file)) { + abilityConfigList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityConfigData.class).getType()); + } catch (Exception e) { + e.printStackTrace(); + continue; + } + + for (AbilityConfigData data : abilityConfigList) { + if (data.Default.modifiers == null || data.Default.modifiers.size() == 0) { + continue; + } + + AbilityModifierEntry modifierEntry = new AbilityModifierEntry(data.Default.abilityName); + + for (Entry<String, AbilityModifier> entry : data.Default.modifiers.entrySet()) { + AbilityModifier modifier = entry.getValue(); + + // Stare. + if (modifier.onAdded != null) { + for (AbilityModifierAction action : modifier.onAdded) { + if (action.$type.contains("HealHP")) { + action.type = AbilityModifierActionType.HealHP; + modifierEntry.getOnAdded().add(action); + } + } + } + + if (modifier.onThinkInterval != null) { + for (AbilityModifierAction action : modifier.onThinkInterval) { + if (action.$type.contains("HealHP")) { + action.type = AbilityModifierActionType.HealHP; + modifierEntry.getOnThinkInterval().add(action); + } + } + } + + if (modifier.onRemoved != null) { + for (AbilityModifierAction action : modifier.onRemoved) { + if (action.$type.contains("HealHP")) { + action.type = AbilityModifierActionType.HealHP; + modifierEntry.getOnRemoved().add(action); + } + } + } + } + + GameData.getAbilityModifiers().put(modifierEntry.getName(), modifierEntry); + } + } + } + private static void loadSpawnData() { // Read from cached file if exists File spawnDataEntries = new File(Grasscutter.getConfig().DATA_FOLDER + "Spawns.json"); diff --git a/src/main/java/emu/grasscutter/data/custom/AbilityModifier.java b/src/main/java/emu/grasscutter/data/custom/AbilityModifier.java new file mode 100644 index 000000000..5a1394c65 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/AbilityModifier.java @@ -0,0 +1,36 @@ +package emu.grasscutter.data.custom; + +import java.util.Map; + +public class AbilityModifier { + public AbilityModifierAction[] onAdded; + public AbilityModifierAction[] onThinkInterval; + public AbilityModifierAction[] onRemoved; + + public static class AbilityConfigData { + public AbilityData Default; + } + + public static class AbilityData { + public String abilityName; + public Map<String, AbilityModifier> modifiers; + } + + public static class AbilityModifierAction { + public String $type; + public AbilityModifierActionType type; + public String target; + public AbilityModifierValue amount; + public AbilityModifierValue amountByTargetCurrentHPRatio; + } + + public static class AbilityModifierValue { + public boolean isFormula; + public boolean isDynamic; + public String dynamicKey; + } + + public enum AbilityModifierActionType { + HealHP, ApplyModifier, LoseHP; + } +} diff --git a/src/main/java/emu/grasscutter/data/custom/AbilityModifierEntry.java b/src/main/java/emu/grasscutter/data/custom/AbilityModifierEntry.java new file mode 100644 index 000000000..b31e0eefe --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/AbilityModifierEntry.java @@ -0,0 +1,37 @@ +package emu.grasscutter.data.custom; + +import java.util.ArrayList; +import java.util.List; + +import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; + +public class AbilityModifierEntry { + private String name; // Custom value + public List<AbilityModifierAction> onModifierAdded; + public List<AbilityModifierAction> onThinkInterval; + public List<AbilityModifierAction> onRemoved; + + public AbilityModifierEntry(String name) { + this.name = name; + this.onModifierAdded = new ArrayList<>(); + this.onThinkInterval = new ArrayList<>(); + this.onRemoved = new ArrayList<>(); + } + + public String getName() { + return name; + } + + public List<AbilityModifierAction> getOnAdded() { + return onModifierAdded; + } + + public List<AbilityModifierAction> getOnThinkInterval() { + return onThinkInterval; + } + + public List<AbilityModifierAction> getOnRemoved() { + return onRemoved; + } + +} diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java new file mode 100644 index 000000000..72c235b94 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java @@ -0,0 +1,185 @@ +package emu.grasscutter.game.ability; + +import com.google.protobuf.InvalidProtocolBufferException; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.AbilityModifier; +import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; +import emu.grasscutter.data.custom.AbilityModifierEntry; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.AbilityInvokeArgumentOuterClass.AbilityInvokeArgument; +import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead; +import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; +import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange; +import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap; +import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType; +import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry; +import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; +import emu.grasscutter.utils.Utils; + +public class AbilityManager { + private Player player; + + public AbilityManager(Player player) { + this.player = player; + } + + public Player getPlayer() { + return this.player; + } + + public void onAbilityInvoke(AbilityInvokeEntry invoke) throws Exception { + //System.out.println(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() + "): " + Utils.bytesToHex(invoke.toByteArray())); + switch (invoke.getArgumentType()) { + case ABILITY_META_OVERRIDE_PARAM: + handleOverrideParam(invoke); + break; + case ABILITY_META_REINIT_OVERRIDEMAP: + handleReinitOverrideMap(invoke); + break; + case ABILITY_META_MODIFIER_CHANGE: + handleModifierChange(invoke); + break; + case ABILITY_MIXIN_COST_STAMINA: + handleMixinCostStamina(invoke); + break; + case ABILITY_ACTION_GENERATE_ELEM_BALL: + handleGenerateElemBall(invoke); + break; + default: + break; + } + } + + private void handleOverrideParam(AbilityInvokeEntry invoke) throws Exception { + GameEntity entity = player.getScene().getEntityById(invoke.getEntityId()); + + if (entity == null) { + return; + } + + AbilityScalarValueEntry entry = AbilityScalarValueEntry.parseFrom(invoke.getAbilityData()); + + entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue()); + } + + private void handleReinitOverrideMap(AbilityInvokeEntry invoke) throws Exception { + GameEntity entity = player.getScene().getEntityById(invoke.getEntityId()); + + if (entity == null) { + return; + } + + AbilityMetaReInitOverrideMap map = AbilityMetaReInitOverrideMap.parseFrom(invoke.getAbilityData()); + + for (AbilityScalarValueEntry entry : map.getOverrideMapList()) { + entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue()); + } + } + + private void handleModifierChange(AbilityInvokeEntry invoke) throws Exception { + GameEntity target = player.getScene().getEntityById(invoke.getEntityId()); + if (target == null) { + return; + } + + AbilityInvokeEntryHead head = invoke.getHead(); + if (head == null) { + return; + } + + AbilityMetaModifierChange data = AbilityMetaModifierChange.parseFrom(invoke.getAbilityData()); + if (data == null) { + return; + } + + GameEntity sourceEntity = player.getScene().getEntityById(data.getApplyEntityId()); + if (sourceEntity == null) { + return; + } + + // This is not how it works but we will keep it for now since healing abilities dont work properly anyways + if (data.getAction() == ModifierAction.ADDED && data.getParentAbilityName() != null) { + // Handle add modifier here + String modifierString = data.getParentAbilityName().getStr(); + AbilityModifierEntry modifier = GameData.getAbilityModifiers().get(modifierString); + + if (modifier != null && modifier.getOnAdded().size() > 0) { + for (AbilityModifierAction action : modifier.getOnAdded()) { + invokeAction(action, target, sourceEntity); + } + } + + // Add to meta modifier list + target.getMetaModifiers().put(head.getInstancedModifierId(), modifierString); + } else if (data.getAction() == ModifierAction.REMOVED) { + String modifierString = target.getMetaModifiers().get(head.getInstancedModifierId()); + + if (modifierString != null) { + // Get modifier and call on remove event + AbilityModifierEntry modifier = GameData.getAbilityModifiers().get(modifierString); + + if (modifier != null && modifier.getOnRemoved().size() > 0) { + for (AbilityModifierAction action : modifier.getOnRemoved()) { + invokeAction(action, target, sourceEntity); + } + } + + // Remove from meta modifiers + target.getMetaModifiers().remove(head.getInstancedModifierId()); + } + } + } + + private void handleMixinCostStamina(AbilityInvokeEntry invoke) { + // Not the right way of doing this + if (Grasscutter.getConfig().OpenStamina) { + // getPlayer().getStaminaManager().updateStamina(getPlayer().getSession(), -450); + // TODO + // set flag in stamina/movement manager that specifies the player is currently using an alternate sprint + } + } + + private void handleGenerateElemBall(AbilityInvokeEntry invoke) { + // TODO create elemental energy orbs + } + + private void invokeAction(AbilityModifierAction action, GameEntity target, GameEntity sourceEntity) { + switch (action.type) { + case HealHP -> { + if (action.amount == null) { + return; + } + + float healAmount = 0; + + if (action.amount.isDynamic && action.amount.dynamicKey != null) { + healAmount = sourceEntity.getMetaOverrideMap().getOrDefault(action.amount.dynamicKey, 0f); + } + + if (healAmount > 0) { + target.heal(healAmount); + } + } + case LoseHP -> { + if (action.amountByTargetCurrentHPRatio == null) { + return; + } + + float damageAmount = 0; + + if (action.amount.isDynamic && action.amount.dynamicKey != null) { + damageAmount = sourceEntity.getMetaOverrideMap().getOrDefault(action.amount.dynamicKey, 0f); + } + + if (damageAmount > 0) { + target.damage(damageAmount); + } + } + } + } +} + diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index 82efb795f..3c8ef2ba9 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -17,17 +17,21 @@ import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlo import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo; import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.utils.Position; import emu.grasscutter.utils.ProtoHelper; import emu.grasscutter.utils.Utils; @@ -110,6 +114,19 @@ public class EntityAvatar extends GameEntity { this.killedBy = killerId; } + @Override + public float heal(float amount) { + float healed = super.heal(amount); + + if (healed > 0f) { + getScene().broadcastPacket( + new PacketEntityFightPropChangeReasonNotify(this, FightProperty.FIGHT_PROP_CUR_HP, healed, PropChangeReason.PROP_CHANGE_ABILITY, ChangeHpReason.ChangeHpAddAbility) + ); + } + + return healed; + } + public SceneAvatarInfo getSceneAvatarInfo() { SceneAvatarInfo.Builder avatarInfo = SceneAvatarInfo.newBuilder() .setUid(this.getPlayer().getUid()) diff --git a/src/main/java/emu/grasscutter/game/entity/GameEntity.java b/src/main/java/emu/grasscutter/game/entity/GameEntity.java index 627b41103..0cdcc3ebd 100644 --- a/src/main/java/emu/grasscutter/game/entity/GameEntity.java +++ b/src/main/java/emu/grasscutter/game/entity/GameEntity.java @@ -1,5 +1,8 @@ package emu.grasscutter.game.entity; +import java.util.HashMap; +import java.util.Map; + import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.world.Scene; @@ -9,8 +12,11 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.utils.Position; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; public abstract class GameEntity { protected int id; @@ -25,6 +31,10 @@ public abstract class GameEntity { private int lastMoveSceneTimeMs; private int lastMoveReliableSeq; + // Abilities + private Map<String, Float> metaOverrideMap; + private Int2ObjectMap<String> metaModifiers; + public GameEntity(Scene scene) { this.scene = scene; this.moveState = MotionState.MOTION_NONE; @@ -54,6 +64,20 @@ public abstract class GameEntity { return isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD; } + public Map<String, Float> getMetaOverrideMap() { + if (this.metaOverrideMap == null) { + this.metaOverrideMap = new HashMap<>(); + } + return this.metaOverrideMap; + } + + public Int2ObjectMap<String> getMetaModifiers() { + if (this.metaModifiers == null) { + this.metaModifiers = new Int2ObjectOpenHashMap<>(); + } + return this.metaModifiers; + } + public abstract Int2FloatOpenHashMap getFightProperties(); public abstract Position getPosition(); @@ -146,4 +170,53 @@ public abstract class GameEntity { public void setSpawnEntry(SpawnDataEntry spawnEntry) { this.spawnEntry = spawnEntry; } + + public float heal(float amount) { + if (this.getFightProperties() == null) { + return 0f; + } + + float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + + if (curHp >= maxHp) { + return 0f; + } + + float healed = Math.min(maxHp - curHp, amount); + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed); + + getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + return healed; + } + + public void damage(float amount) { + damage(amount, 0); + } + + public void damage(float amount, int killerId) { + // Sanity check + if (getFightProperties() == null) { + return; + } + + // Lose hp + addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -amount); + + // Check if dead + boolean isDead = false; + if (getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { + setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); + isDead = true; + } + + // Packets + this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + // Check if dead + if (isDead) { + getScene().killEntity(this, 0); + } + } } diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 71ae9d8c6..477f974ea 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -8,6 +8,7 @@ import emu.grasscutter.data.def.PlayerLevelData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.game.CoopRequest; +import emu.grasscutter.game.ability.AbilityManager; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.AvatarProfileData; import emu.grasscutter.game.avatar.AvatarStorage; @@ -89,7 +90,8 @@ public class Player { @Transient private FriendsList friendsList; @Transient private MailHandler mailHandler; @Transient private MessageHandler messageHandler; - + @Transient private AbilityManager abilityManager; + @Transient private SotSManager sotsManager; private TeamManager teamManager; @@ -142,6 +144,7 @@ public class Player { this.friendsList = new FriendsList(this); this.mailHandler = new MailHandler(this); this.towerManager = new TowerManager(this); + this.abilityManager = new AbilityManager(this); this.pos = new Position(); this.rotation = new Position(); this.properties = new HashMap<>(); @@ -1025,6 +1028,10 @@ public class Player { public SotSManager getSotSManager() { return sotsManager; } + public AbilityManager getAbilityManager() { + return abilityManager; + } + public synchronized void onTick() { // Check ping if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 82ce9139f..daed26e3e 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -385,27 +385,7 @@ public class Scene { } // Sanity check - if (target.getFightProperties() == null) { - return; - } - - // Lose hp - target.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -result.getDamage()); - - // Check if dead - boolean isDead = false; - if (target.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { - target.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); - isDead = true; - } - - // Packets - this.broadcastPacket(new PacketEntityFightPropUpdateNotify(target, FightProperty.FIGHT_PROP_CUR_HP)); - - // Check if dead - if (isDead) { - this.killEntity(target, result.getAttackerId()); - } + target.damage(result.getDamage(), result.getAttackerId()); } public void killEntity(GameEntity target, int attackerId) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java index 710ea0fea..8be2d1c1f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java @@ -6,6 +6,7 @@ import emu.grasscutter.net.proto.AbilityInvocationsNotifyOuterClass.AbilityInvoc import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.Utils; @Opcodes(PacketOpcodes.AbilityInvocationsNotify) public class HandlerAbilityInvocationsNotify extends PacketHandler { @@ -15,7 +16,7 @@ public class HandlerAbilityInvocationsNotify extends PacketHandler { AbilityInvocationsNotify notif = AbilityInvocationsNotify.parseFrom(payload); for (AbilityInvokeEntry entry : notif.getInvokesList()) { - //System.out.println(entry.getArgumentType() + ": " + Utils.bytesToHex(entry.getAbilityData().toByteArray())); + session.getPlayer().getAbilityManager().onAbilityInvoke(entry); session.getPlayer().getAbilityInvokeHandler().addEntry(entry.getForwardType(), entry); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java index cfe697b91..a1035af85 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityInitFinishNotify.java @@ -6,6 +6,7 @@ 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; +import emu.grasscutter.utils.Utils; @Opcodes(PacketOpcodes.ClientAbilityInitFinishNotify) public class HandlerClientAbilityInitFinishNotify extends PacketHandler { @@ -15,6 +16,7 @@ public class HandlerClientAbilityInitFinishNotify extends PacketHandler { ClientAbilityInitFinishNotify notif = ClientAbilityInitFinishNotify.parseFrom(payload); for (AbilityInvokeEntry entry : notif.getInvokesList()) { + session.getPlayer().getAbilityManager().onAbilityInvoke(entry); session.getPlayer().getClientAbilityInitFinishHandler().addEntry(entry.getForwardType(), entry); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtCreateGadgetNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtCreateGadgetNotify.java index 9b1cdb0fb..92229d400 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtCreateGadgetNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtCreateGadgetNotify.java @@ -14,11 +14,6 @@ public class HandlerEvtCreateGadgetNotify extends PacketHandler { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { EvtCreateGadgetNotify notify = EvtCreateGadgetNotify.parseFrom(payload); - // Dont handle in singleplayer - if (!session.getPlayer().getWorld().isMultiplayer()) { - return; - } - // Sanity check - dont add duplicate entities if (session.getPlayer().getScene().getEntityById(notify.getEntityId()) != null) { return; diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDestroyGadgetNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDestroyGadgetNotify.java index 608215d0a..7d1abe8ea 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDestroyGadgetNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDestroyGadgetNotify.java @@ -12,11 +12,6 @@ public class HandlerEvtDestroyGadgetNotify extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { EvtDestroyGadgetNotify notify = EvtDestroyGadgetNotify.parseFrom(payload); - - // Dont handle in singleplayer - if (!session.getPlayer().getWorld().isMultiplayer()) { - return; - } session.getPlayer().getScene().onPlayerDestroyGadget(notify.getEntityId()); } 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 5151034f2..6c4d86f7e 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetEntityClientDataNotify.java @@ -23,7 +23,7 @@ public class HandlerSetEntityClientDataNotify extends PacketHandler { BasePacket packet = new BasePacket(PacketOpcodes.SetEntityClientDataNotify, true); packet.setData(notif); - session.getPlayer().getScene().broadcastPacketToOthers(session.getPlayer(), packet); + session.getPlayer().getScene().broadcastPacket(packet); } } From 1f86e7bb1776db9c40dddadf5c85bd02ef5f5cf7 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sun, 8 May 2022 05:31:53 -0700 Subject: [PATCH 206/434] Implement energy balls (orbs) --- .../game/ability/AbilityManager.java | 21 +++++++++- .../emu/grasscutter/game/avatar/Avatar.java | 26 ++++++++---- .../game/avatar/AvatarStorage.java | 5 ++- .../grasscutter/game/entity/EntityAvatar.java | 19 ++++++++- .../grasscutter/game/inventory/Inventory.java | 3 ++ .../grasscutter/game/player/TeamManager.java | 19 +++++++++ .../grasscutter/game/props/ElementType.java | 42 +++++++++++-------- .../recv/HandlerSetPlayerBornDataReq.java | 2 +- ...cketEntityFightPropChangeReasonNotify.java | 22 +++++++++- 9 files changed, 128 insertions(+), 31 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java index 72c235b94..d1ae388ea 100644 --- a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java +++ b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java @@ -7,9 +7,12 @@ import emu.grasscutter.data.GameData; import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; +import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.custom.AbilityModifierEntry; +import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; import emu.grasscutter.net.proto.AbilityInvokeArgumentOuterClass.AbilityInvokeArgument; import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; @@ -18,6 +21,7 @@ import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityM import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType; import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry; import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; +import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Utils; public class AbilityManager { @@ -143,8 +147,21 @@ public class AbilityManager { } } - private void handleGenerateElemBall(AbilityInvokeEntry invoke) { - // TODO create elemental energy orbs + private void handleGenerateElemBall(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { + AbilityActionGenerateElemBall action = AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData()); + if (action == null) { + return; + } + + ItemData itemData = GameData.getItemDataMap().get(2024); + if (itemData == null) { + return; // Should never happen + } + + EntityItem energyBall = new EntityItem(getPlayer().getScene(), getPlayer(), itemData, new Position(action.getPos()), 1); + energyBall.getRotation().set(action.getRot()); + + getPlayer().getScene().addEntity(energyBall); } private void invokeAction(AbilityModifierAction action, GameEntity target, GameEntity sourceEntity) { diff --git a/src/main/java/emu/grasscutter/game/avatar/Avatar.java b/src/main/java/emu/grasscutter/game/avatar/Avatar.java index b0bfb0801..c4fb85671 100644 --- a/src/main/java/emu/grasscutter/game/avatar/Avatar.java +++ b/src/main/java/emu/grasscutter/game/avatar/Avatar.java @@ -69,6 +69,7 @@ public class Avatar { @Transient private Player owner; @Transient private AvatarData data; + @Transient private AvatarSkillDepotData skillDepot; @Transient private long guid; // Player unique id private int avatarId; // Id of avatar @@ -103,8 +104,8 @@ public class Avatar { private int nameCardRewardId; private int nameCardId; + @Deprecated // Do not use. Morhpia only! public Avatar() { - // Morhpia only! this.equips = new Int2ObjectOpenHashMap<>(); this.fightProp = new Int2FloatOpenHashMap(); this.extraAbilityEmbryos = new HashSet<>(); @@ -140,7 +141,7 @@ public class Avatar { } // Skill depot - this.setSkillDepot(getAvatarData().getSkillDepot()); + this.setSkillDepotData(getAvatarData().getSkillDepot()); // Set stats this.recalcStats(); @@ -164,7 +165,8 @@ public class Avatar { } protected void setAvatarData(AvatarData data) { - this.data = data; + if (this.data != null) return; + this.data = data; // Used while loading this from the database } public int getOwnerId() { @@ -257,9 +259,19 @@ public class Avatar { return skillDepotId; } - public void setSkillDepot(AvatarSkillDepotData skillDepot) { - // Set id + public AvatarSkillDepotData getSkillDepot() { + return skillDepot; + } + + protected void setSkillDepot(AvatarSkillDepotData skillDepot) { + if (this.skillDepot != null) return; + this.skillDepot = skillDepot; // Used while loading this from the database + } + + public void setSkillDepotData(AvatarSkillDepotData skillDepot) { + // Set id and depot this.skillDepotId = skillDepot.getId(); + this.skillDepot = skillDepot; // Clear, then add skills getSkillLevelMap().clear(); if (skillDepot.getEnergySkill() > 0) { @@ -501,8 +513,8 @@ public class Avatar { // Set energy usage if (data.getSkillDepot() != null && data.getSkillDepot().getEnergySkillData() != null) { ElementType element = data.getSkillDepot().getElementType(); - this.setFightProperty(element.getEnergyProperty(), data.getSkillDepot().getEnergySkillData().getCostElemVal()); - this.setFightProperty((element.getEnergyProperty().getId() % 70) + 1000, data.getSkillDepot().getEnergySkillData().getCostElemVal()); + this.setFightProperty(element.getMaxEnergyProp(), data.getSkillDepot().getEnergySkillData().getCostElemVal()); + this.setFightProperty((element.getMaxEnergyProp().getId() % 70) + 1000, data.getSkillDepot().getEnergySkillData().getCostElemVal()); } // Artifacts diff --git a/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java b/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java index 2486e36ab..de3221473 100644 --- a/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java +++ b/src/main/java/emu/grasscutter/game/avatar/AvatarStorage.java @@ -5,6 +5,7 @@ import java.util.List; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.AvatarData; +import emu.grasscutter.data.def.AvatarSkillDepotData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.inventory.GameItem; @@ -139,12 +140,14 @@ public class AvatarStorage implements Iterable<Avatar> { } AvatarData avatarData = GameData.getAvatarDataMap().get(avatar.getAvatarId()); - if (avatarData == null) { + AvatarSkillDepotData skillDepot = GameData.getAvatarSkillDepotDataMap().get(avatar.getSkillDepotId()); + if (avatarData == null || skillDepot == null) { continue; } // Set ownerships avatar.setAvatarData(avatarData); + avatar.setSkillDepot(skillDepot); avatar.setOwner(getPlayer()); // Force recalc of const boosted skills diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index 3c8ef2ba9..858db3de2 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -30,6 +30,7 @@ import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.utils.Position; @@ -127,6 +128,22 @@ public class EntityAvatar extends GameEntity { return healed; } + public void addEnergy(float amount) { + FightProperty curEnergyProp = getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); + FightProperty maxEnergyProp = getAvatar().getSkillDepot().getElementType().getMaxEnergyProp(); + + float curEnergy = this.getFightProperty(curEnergyProp); + float maxEnergy = this.getFightProperty(maxEnergyProp); + float newEnergy = Math.min(curEnergy + amount, maxEnergy); + + if (newEnergy != curEnergy) { + setFightProperty(curEnergyProp, newEnergy); + + getScene().broadcastPacket(new PacketAvatarFightPropUpdateNotify(getAvatar(), curEnergyProp)); + getScene().broadcastPacket(new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, PropChangeReason.PROP_CHANGE_ENERGY_BALL)); + } + } + public SceneAvatarInfo getSceneAvatarInfo() { SceneAvatarInfo.Builder avatarInfo = SceneAvatarInfo.newBuilder() .setUid(this.getPlayer().getUid()) @@ -258,5 +275,5 @@ public class EntityAvatar extends GameEntity { // return abilityControlBlock.build(); - } + } } diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index 14d1ae203..c4158ee6f 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -172,6 +172,9 @@ public class Inventory implements Iterable<GameItem> { // Handle this.addVirtualItem(item.getItemId(), item.getCount()); return item; + } else if (item.getItemData().getMaterialType() == MaterialType.MATERIAL_ADSORBATE) { + player.getTeamManager().addEnergyToTeam(item); + return null; } else if (item.getItemData().getMaterialType() == MaterialType.MATERIAL_AVATAR) { // Get avatar id int avatarId = (item.getItemId() % 1000) + 10000000; diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 204af2976..891d0a215 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -10,6 +10,7 @@ import emu.grasscutter.data.def.AvatarSkillDepotData; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityBaseGadget; +import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.props.ElementType; import emu.grasscutter.game.props.EnterReason; import emu.grasscutter.game.props.FightProperty; @@ -579,6 +580,24 @@ public class TeamManager { // Packets getPlayer().sendPacket(new BasePacket(PacketOpcodes.WorldPlayerReviveRsp)); } + + public synchronized void addEnergyToTeam(GameItem energyBall) { + // TODO + float baseEnergy = 2; + + for (int i = 0; i < getActiveTeam().size(); i++) { + EntityAvatar entity = getActiveTeam().get(i); + + float energyGain = baseEnergy; + + // Active character gets full hp + if (getCurrentCharacterIndex() != i) { + energyGain *= Math.max(1.0 - (getActiveTeam().size() * .1f), .6f); + } + + entity.addEnergy(energyGain); + } + } public void saveAvatars() { // Save all avatars from active team diff --git a/src/main/java/emu/grasscutter/game/props/ElementType.java b/src/main/java/emu/grasscutter/game/props/ElementType.java index 23362c39f..12a30f6fc 100644 --- a/src/main/java/emu/grasscutter/game/props/ElementType.java +++ b/src/main/java/emu/grasscutter/game/props/ElementType.java @@ -9,21 +9,22 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; public enum ElementType { - None (0, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), - Fire (1, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10101, "TeamResonance_Fire_Lv2"), - Water (2, FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, 10201, "TeamResonance_Water_Lv2"), - Grass (3, FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY), - Electric (4, FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, 10401, "TeamResonance_Electric_Lv2"), - Ice (5, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, 10601, "TeamResonance_Ice_Lv2"), - Frozen (6, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY), - Wind (7, FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, 10301, "TeamResonance_Wind_Lv2"), - Rock (8, FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, 10701, "TeamResonance_Rock_Lv2"), - AntiFire (9, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), - Default (255, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10801, "TeamResonance_AllDifferent"); + None (0, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), + Fire (1, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10101, "TeamResonance_Fire_Lv2"), + Water (2, FightProperty.FIGHT_PROP_CUR_WATER_ENERGY, FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, 10201, "TeamResonance_Water_Lv2"), + Grass (3, FightProperty.FIGHT_PROP_CUR_GRASS_ENERGY, FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY), + Electric (4, FightProperty.FIGHT_PROP_CUR_ELEC_ENERGY, FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, 10401, "TeamResonance_Electric_Lv2"), + Ice (5, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, 10601, "TeamResonance_Ice_Lv2"), + Frozen (6, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY), + Wind (7, FightProperty.FIGHT_PROP_CUR_WIND_ENERGY, FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, 10301, "TeamResonance_Wind_Lv2"), + Rock (8, FightProperty.FIGHT_PROP_CUR_ROCK_ENERGY, FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, 10701, "TeamResonance_Rock_Lv2"), + AntiFire (9, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), + Default (255, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10801, "TeamResonance_AllDifferent"); private final int value; private final int teamResonanceId; - private final FightProperty energyProperty; + private final FightProperty curEnergyProp; + private final FightProperty maxEnergyProp; private final int configHash; private static final Int2ObjectMap<ElementType> map = new Int2ObjectOpenHashMap<>(); private static final Map<String, ElementType> stringMap = new HashMap<>(); @@ -35,13 +36,14 @@ public enum ElementType { }); } - private ElementType(int value, FightProperty energyProperty) { - this(value, energyProperty, 0, null); + private ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp) { + this(value, curEnergyProp, maxEnergyProp, 0, null); } - private ElementType(int value, FightProperty energyProperty, int teamResonanceId, String configName) { + private ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp, int teamResonanceId, String configName) { this.value = value; - this.energyProperty = energyProperty; + this.curEnergyProp = curEnergyProp; + this.maxEnergyProp = maxEnergyProp; this.teamResonanceId = teamResonanceId; if (configName != null) { this.configHash = Utils.abilityHash(configName); @@ -54,8 +56,12 @@ public enum ElementType { return value; } - public FightProperty getEnergyProperty() { - return energyProperty; + public FightProperty getCurEnergyProp() { + return curEnergyProp; + } + + public FightProperty getMaxEnergyProp() { + return maxEnergyProp; } public int getTeamResonanceId() { 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 2487df063..53d141a99 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java @@ -62,7 +62,7 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler { // Create avatar if (player.getAvatars().getAvatarCount() == 0) { Avatar mainCharacter = new Avatar(avatarId); - mainCharacter.setSkillDepot(GameData.getAvatarSkillDepotDataMap().get(startingSkillDepot)); + mainCharacter.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(startingSkillDepot)); player.addAvatar(mainCharacter); player.setMainCharacterId(avatarId); player.setHeadImage(avatarId); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropChangeReasonNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropChangeReasonNotify.java index 5778f711a..366354a40 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropChangeReasonNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropChangeReasonNotify.java @@ -11,21 +11,27 @@ import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; import java.util.List; public class PacketEntityFightPropChangeReasonNotify extends BasePacket { + public PacketEntityFightPropChangeReasonNotify(GameEntity entity, FightProperty prop, Float value, List<Integer> param, PropChangeReason reason, ChangeHpReason changeHpReason) { super(PacketOpcodes.EntityFightPropChangeReasonNotify); + EntityFightPropChangeReasonNotify.Builder proto = EntityFightPropChangeReasonNotify.newBuilder() .setEntityId(entity.getId()) .setPropType(prop.getId()) .setPropDelta(value) .setReason(reason) .setChangeHpReason(changeHpReason); - for(int p: param){ + + for(int p : param){ proto.addParamList(p); } + this.setData(proto); } + public PacketEntityFightPropChangeReasonNotify(GameEntity entity, FightProperty prop, Float value, PropChangeReason reason, ChangeHpReason changeHpReason) { super(PacketOpcodes.EntityFightPropChangeReasonNotify); + EntityFightPropChangeReasonNotify proto = EntityFightPropChangeReasonNotify.newBuilder() .setEntityId(entity.getId()) .setPropType(prop.getId()) @@ -33,6 +39,20 @@ public class PacketEntityFightPropChangeReasonNotify extends BasePacket { .setReason(reason) .setChangeHpReason(changeHpReason) .build(); + + this.setData(proto); + } + + public PacketEntityFightPropChangeReasonNotify(GameEntity entity, FightProperty prop, Float value, PropChangeReason reason) { + super(PacketOpcodes.EntityFightPropChangeReasonNotify); + + EntityFightPropChangeReasonNotify proto = EntityFightPropChangeReasonNotify.newBuilder() + .setEntityId(entity.getId()) + .setPropType(prop.getId()) + .setPropDelta(value) + .setReason(reason) + .build(); + this.setData(proto); } } From b9fbc4975d4a2c8499cd2384d62614a97e5f30a9 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Sun, 8 May 2022 05:39:12 -0700 Subject: [PATCH 207/434] Optimize invoke packet handling --- .../java/emu/grasscutter/game/player/InvokeHandler.java | 2 +- .../packet/recv/HandlerAbilityInvocationsNotify.java | 4 ---- .../packet/recv/HandlerCombatInvocationsNotify.java | 8 -------- .../server/packet/recv/HandlerUnionCmdNotify.java | 9 +++++++++ 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/player/InvokeHandler.java b/src/main/java/emu/grasscutter/game/player/InvokeHandler.java index edfcbfc83..b8a9ed89f 100644 --- a/src/main/java/emu/grasscutter/game/player/InvokeHandler.java +++ b/src/main/java/emu/grasscutter/game/player/InvokeHandler.java @@ -30,7 +30,7 @@ public class InvokeHandler<T> { } public synchronized void update(Player player) { - if (player.getWorld() == null) { + if (player.getWorld() == null || player.getScene() == null) { this.entryListForwardAll.clear(); this.entryListForwardAllExceptCur.clear(); this.entryListForwardHost.clear(); diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java index 8be2d1c1f..a5d4c7f36 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAbilityInvocationsNotify.java @@ -19,10 +19,6 @@ public class HandlerAbilityInvocationsNotify extends PacketHandler { session.getPlayer().getAbilityManager().onAbilityInvoke(entry); session.getPlayer().getAbilityInvokeHandler().addEntry(entry.getForwardType(), entry); } - - if (notif.getInvokesList().size() > 0) { - session.getPlayer().getAbilityInvokeHandler().update(session.getPlayer()); - } } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 36252f828..50fca5101 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -74,14 +74,6 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { session.getPlayer().getCombatInvokeHandler().addEntry(entry.getForwardType(), entry); } - - if (notif.getInvokeListList().size() > 0) { - session.getPlayer().getCombatInvokeHandler().update(session.getPlayer()); - } - // Handle attack results last - while (!session.getPlayer().getAttackResults().isEmpty()) { - session.getPlayer().getScene().handleAttack(session.getPlayer().getAttackResults().poll()); - } } private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnionCmdNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnionCmdNotify.java index 1f4a9e7f3..2468f675f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnionCmdNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnionCmdNotify.java @@ -15,5 +15,14 @@ public class HandlerUnionCmdNotify extends PacketHandler { for (UnionCmd cmd : req.getCmdListList()) { session.getServer().getPacketHandler().handle(session, cmd.getMessageId(), EMPTY_BYTE_ARRAY, cmd.getBody().toByteArray()); } + + // Update + session.getPlayer().getCombatInvokeHandler().update(session.getPlayer()); + session.getPlayer().getAbilityInvokeHandler().update(session.getPlayer()); + + // Handle attack results last + while (!session.getPlayer().getAttackResults().isEmpty()) { + session.getPlayer().getScene().handleAttack(session.getPlayer().getAttackResults().poll()); + } } } From e95431d3e78a1f82009af534ba5f03ff5c3a2b01 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sat, 7 May 2022 21:47:13 +0800 Subject: [PATCH 208/434] Monsters tide turn by turn && Ban User Skill && Lua functions --- src/main/resources/logback.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index bd0740fca..5ab1957e5 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -24,4 +24,6 @@ <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> + <logger name="emu.grasscutter.scripts.ScriptLib" level="DEBUG"> + </logger> </Configuration> \ No newline at end of file From 916b7412fd6fc2a8a8dd25ca7a2d4ad39b420adc Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Sun, 8 May 2022 17:11:02 +0800 Subject: [PATCH 209/434] Persist Tower Data && Set The Tower Schedule --- build.gradle | 2 +- data/TowerSchedule.json | 5 ++ .../command/commands/UnlockTowerCommand.java | 32 +++++++ .../java/emu/grasscutter/data/GameData.java | 4 + .../data/def/TowerScheduleData.java | 70 ++++++++++++++++ .../dungeons/TowerDungeonSettleListener.java | 16 ++-- .../game/tower/TowerLevelRecord.java | 64 ++++++++++++++ .../grasscutter/game/tower/TowerManager.java | 83 +++++++++++++++---- .../game/tower/TowerScheduleConfig.java | 35 ++++++++ .../game/tower/TowerScheduleManager.java | 75 +++++++++++++++++ .../scripts/SceneScriptManager.java | 29 ++++--- .../emu/grasscutter/scripts/ScriptLib.java | 32 ++++--- .../grasscutter/server/game/GameServer.java | 8 +- .../packet/recv/HandlerTowerAllDataReq.java | 5 +- .../send/PacketDungeonSettleNotify.java | 2 +- .../packet/send/PacketTowerAllDataRsp.java | 57 +++++++++---- .../PacketTowerFloorRecordChangeNotify.java | 6 +- .../send/PacketTowerLevelStarCondNotify.java | 32 +++++++ .../emu/grasscutter/utils/DateHelper.java | 6 +- src/main/resources/languages/en-US.json | 3 + 20 files changed, 500 insertions(+), 66 deletions(-) create mode 100644 data/TowerSchedule.json create mode 100644 src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java create mode 100644 src/main/java/emu/grasscutter/data/def/TowerScheduleData.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java create mode 100644 src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java diff --git a/build.gradle b/build.gradle index eefef5b48..dd8fa9fa0 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' diff --git a/data/TowerSchedule.json b/data/TowerSchedule.json new file mode 100644 index 000000000..b93100645 --- /dev/null +++ b/data/TowerSchedule.json @@ -0,0 +1,5 @@ +{ + "scheduleId" : 1, + "scheduleStartTime" : "2022-05-01T00:00:00+08:00", + "nextScheduleChangeTime" : "2022-05-30T00:00:00+08:00" +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java new file mode 100644 index 000000000..e0fce695c --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java @@ -0,0 +1,32 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.tower.TowerLevelRecord; + +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "unlocktower", usage = "unlocktower", aliases = {"ut"}, + description = "Unlock all levels of tower", permission = "player.tower") +public class UnlockTowerCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List<String> args) { + unlockFloor(sender, sender.getServer().getTowerScheduleManager() + .getCurrentTowerScheduleData().getEntranceFloorId()); + + unlockFloor(sender, sender.getServer().getTowerScheduleManager() + .getScheduleFloors()); + + CommandHandler.sendMessage(sender, translate("commands.tower.unlock_done")); + } + + public void unlockFloor(Player player, List<Integer> floors){ + floors.stream() + .filter(id -> !player.getTowerManager().getRecordMap().containsKey(id)) + .forEach(id -> player.getTowerManager().getRecordMap().put(id, new TowerLevelRecord(id))); + } +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index f06d1bda6..ac2472192 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -73,6 +73,7 @@ public class GameData { private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap<TowerScheduleData> towerScheduleDataMap = new Int2ObjectOpenHashMap<>(); // Cache private static Map<Integer, List<Integer>> fetters = new HashMap<>(); @@ -327,4 +328,7 @@ public class GameData { public static Int2ObjectMap<TowerLevelData> getTowerLevelDataMap(){ return towerLevelDataMap; } + public static Int2ObjectMap<TowerScheduleData> getTowerScheduleDataMap(){ + return towerScheduleDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java b/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java new file mode 100644 index 000000000..017776c06 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/TowerScheduleData.java @@ -0,0 +1,70 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +import java.util.List; + +@ResourceType(name = "TowerScheduleExcelConfigData.json") +public class TowerScheduleData extends GameResource { + private int ScheduleId; + private List<Integer> EntranceFloorId; + private List<ScheduleDetail> Schedules; + private int MonthlyLevelConfigId; + @Override + public int getId() { + return ScheduleId; + } + + @Override + public void onLoad() { + super.onLoad(); + this.Schedules = this.Schedules.stream() + .filter(item -> item.getFloorList().size() > 0) + .toList(); + } + + public int getScheduleId() { + return ScheduleId; + } + + public void setScheduleId(int scheduleId) { + ScheduleId = scheduleId; + } + + public List<Integer> getEntranceFloorId() { + return EntranceFloorId; + } + + public void setEntranceFloorId(List<Integer> entranceFloorId) { + EntranceFloorId = entranceFloorId; + } + + public List<ScheduleDetail> getSchedules() { + return Schedules; + } + + public void setSchedules(List<ScheduleDetail> schedules) { + Schedules = schedules; + } + + public int getMonthlyLevelConfigId() { + return MonthlyLevelConfigId; + } + + public void setMonthlyLevelConfigId(int monthlyLevelConfigId) { + MonthlyLevelConfigId = monthlyLevelConfigId; + } + + public static class ScheduleDetail{ + private List<Integer> FloorList; + + public List<Integer> getFloorList() { + return FloorList; + } + + public void setFloorList(List<Integer> floorList) { + FloorList = floorList; + } + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java index 5b1ff7a30..c480d047f 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -12,12 +12,18 @@ public class TowerDungeonSettleListener implements DungeonSettleListener { scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); var towerManager = scene.getPlayers().get(0).getTowerManager(); - towerManager.notifyCurLevelRecordChangeWhenDone(); - scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify(towerManager.getCurrentFloorId())); - scene.broadcastPacket(new PacketDungeonSettleNotify(scene.getChallenge(), - true, + towerManager.notifyCurLevelRecordChangeWhenDone(3); + scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify( + towerManager.getCurrentFloorId(), + 3, + towerManager.canEnterScheduleFloor() + )); + + scene.broadcastPacket(new PacketDungeonSettleNotify( + scene.getChallenge(), + towerManager.hasNextFloor(), towerManager.hasNextLevel(), - towerManager.getNextFloorId() + towerManager.hasNextLevel() ? 0 : towerManager.getNextFloorId() )); } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java b/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java new file mode 100644 index 000000000..5a65f63ed --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerLevelRecord.java @@ -0,0 +1,64 @@ +package emu.grasscutter.game.tower; + +import dev.morphia.annotations.Entity; + +import java.util.HashMap; +import java.util.Map; + +@Entity +public class TowerLevelRecord { + /** + * floorId in config + */ + private int floorId; + /** + * LevelId - Stars + */ + private Map<Integer, Integer> passedLevelMap; + + private int floorStarRewardProgress; + + public TowerLevelRecord setLevelStars(int levelId, int stars){ + passedLevelMap.put(levelId, stars); + return this; + } + + public int getStarCount() { + return passedLevelMap.values().stream().mapToInt(Integer::intValue).sum(); + } + + public TowerLevelRecord(){ + + } + + public TowerLevelRecord(int floorId){ + this.floorId = floorId; + this.passedLevelMap = new HashMap<>(); + this.floorStarRewardProgress = 0; + } + + public int getFloorId() { + return floorId; + } + + public void setFloorId(int floorId) { + this.floorId = floorId; + } + + public Map<Integer, Integer> getPassedLevelMap() { + return passedLevelMap; + } + + public void setPassedLevelMap(Map<Integer, Integer> passedLevelMap) { + this.passedLevelMap = passedLevelMap; + } + + public int getFloorStarRewardProgress() { + return floorStarRewardProgress; + } + + public void setFloorStarRewardProgress(int floorStarRewardProgress) { + this.floorStarRewardProgress = floorStarRewardProgress; + } + +} diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 409549a1f..9346ffead 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -9,10 +9,12 @@ import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; - import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; +import emu.grasscutter.server.packet.send.PacketTowerLevelStarCondNotify; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Entity public class TowerManager { @@ -26,11 +28,19 @@ public class TowerManager { this.player = player; } + /** + * the floor players chose + */ private int currentFloorId; private int currentLevel; @Transient private int currentLevelId; + /** + * floorId - Record + */ + private Map<Integer, TowerLevelRecord> recordMap; + @Transient private int entryScene; @@ -38,7 +48,26 @@ public class TowerManager { return currentFloorId; } + public int getCurrentLevelId(){ + return this.currentLevelId + currentLevel; + } + + /** + * form 1-3 + */ + public int getCurrentLevel(){ + return currentLevel + 1; + } private static final List<DungeonSettleListener> towerDungeonSettleListener = List.of(new TowerDungeonSettleListener()); + + public Map<Integer, TowerLevelRecord> getRecordMap() { + if(recordMap == null){ + recordMap = new HashMap<>(); + recordMap.put(1001, new TowerLevelRecord(1001)); + } + return recordMap; + } + public void teamSelect(int floor, List<List<Long>> towerTeams) { var floorData = GameData.getTowerFloorDataMap().get(floor); @@ -54,51 +83,73 @@ public class TowerManager { entryScene = player.getSceneId(); } - player.getTeamManager().setupTemporaryTeam(towerTeams); } public void enterLevel(int enterPointId) { - var levelData = GameData.getTowerLevelDataMap().get(currentLevelId + currentLevel); + var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId()); - this.currentLevel++; - var id = levelData.getDungeonId(); + var dungeonId = levelData.getDungeonId(); notifyCurLevelRecordChange(); // use team user choose player.getTeamManager().useTemporaryTeam(0); - player.getServer().getDungeonManager().handoffDungeon(player, id, + player.getServer().getDungeonManager().handoffDungeon(player, dungeonId, towerDungeonSettleListener); // make sure user can exit dungeon correctly player.getScene().setPrevScene(entryScene); player.getScene().setPrevScenePoint(enterPointId); - player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, currentLevel)); + player.getSession().send(new PacketTowerEnterLevelRsp(currentFloorId, getCurrentLevel())); // stop using skill player.getSession().send(new PacketCanUseSkillNotify(false)); + // notify the cond of stars + player.getSession().send(new PacketTowerLevelStarCondNotify(currentFloorId, getCurrentLevel())); } public void notifyCurLevelRecordChange(){ - player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel)); + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, getCurrentLevel())); } - public void notifyCurLevelRecordChangeWhenDone(){ - player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, currentLevel + 1)); + public void notifyCurLevelRecordChangeWhenDone(int stars){ + if(!recordMap.containsKey(currentFloorId)){ + recordMap.put(currentFloorId, + new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(),stars)); + }else{ + recordMap.put(currentFloorId, + recordMap.get(currentFloorId).setLevelStars(getCurrentLevelId(),stars)); + } + + this.currentLevel++; + + if(!hasNextLevel()){ + // set up the next floor + recordMap.put(getNextFloorId(), new TowerLevelRecord(getNextFloorId())); + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(getNextFloorId(), 1)); + }else{ + player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, getCurrentLevel())); + } } public boolean hasNextLevel(){ return this.currentLevel < 3; } - public int getNextFloorId() { - if(hasNextLevel()){ - return 0; - } - this.currentFloorId++; - return this.currentFloorId; + return player.getServer().getTowerScheduleManager().getNextFloorId(this.currentFloorId); + } + public boolean hasNextFloor(){ + return player.getServer().getTowerScheduleManager().getNextFloorId(this.currentFloorId) > 0; } public void clearEntry() { this.entryScene = 0; } + + public boolean canEnterScheduleFloor(){ + if(!recordMap.containsKey(player.getServer().getTowerScheduleManager().getLastEntranceFloor())){ + return false; + } + return recordMap.get(player.getServer().getTowerScheduleManager().getLastEntranceFloor()) + .getStarCount() >= 6; + } } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java new file mode 100644 index 000000000..35afbc7ba --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleConfig.java @@ -0,0 +1,35 @@ +package emu.grasscutter.game.tower; + +import java.util.Date; + +public class TowerScheduleConfig { + private int scheduleId; + + private Date scheduleStartTime; + private Date nextScheduleChangeTime; + + + public int getScheduleId() { + return scheduleId; + } + + public void setScheduleId(int scheduleId) { + this.scheduleId = scheduleId; + } + + public Date getScheduleStartTime() { + return scheduleStartTime; + } + + public void setScheduleStartTime(Date scheduleStartTime) { + this.scheduleStartTime = scheduleStartTime; + } + + public Date getNextScheduleChangeTime() { + return nextScheduleChangeTime; + } + + public void setNextScheduleChangeTime(Date nextScheduleChangeTime) { + this.nextScheduleChangeTime = nextScheduleChangeTime; + } +} diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java new file mode 100644 index 000000000..33f5c158d --- /dev/null +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java @@ -0,0 +1,75 @@ +package emu.grasscutter.game.tower; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.TowerScheduleData; +import emu.grasscutter.server.game.GameServer; + +import java.io.FileReader; +import java.util.List; + +public class TowerScheduleManager { + private final GameServer gameServer; + + public GameServer getGameServer() { + return gameServer; + } + + public TowerScheduleManager(GameServer gameServer) { + this.gameServer = gameServer; + this.load(); + } + + private TowerScheduleConfig towerScheduleConfig; + + public synchronized void load(){ + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "TowerSchedule.json")) { + towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class); + + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load tower schedule config.", e); + } + } + + public TowerScheduleConfig getTowerScheduleConfig() { + return towerScheduleConfig; + } + + public TowerScheduleData getCurrentTowerScheduleData(){ + var data = GameData.getTowerScheduleDataMap().get(towerScheduleConfig.getScheduleId()); + if(data == null){ + Grasscutter.getLogger().error("Could not get current tower schedule data by config:{}", towerScheduleConfig); + } + return data; + } + + public List<Integer> getScheduleFloors() { + return getCurrentTowerScheduleData().getSchedules().get(0).getFloorList(); + } + + public int getNextFloorId(int floorId){ + var entranceFloors = getCurrentTowerScheduleData().getEntranceFloorId(); + var nextId = 0; + // find in entrance floors first + for(int i=0;i<entranceFloors.size()-1;i++){ + if(floorId == entranceFloors.get(i)){ + nextId = entranceFloors.get(i+1); + } + } + if(nextId != 0){ + return nextId; + } + var scheduleFloors = getScheduleFloors(); + // find in schedule floors + for(int i=0;i<scheduleFloors.size()-1;i++){ + if(floorId == scheduleFloors.get(i)){ + nextId = scheduleFloors.get(i+1); + } + } + return nextId; + } + + public Integer getLastEntranceFloor() { + return getCurrentTowerScheduleData().getEntranceFloorId().get(getCurrentTowerScheduleData().getEntranceFloorId().size()-1); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index b8ba800a6..611a9aa17 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -90,6 +90,10 @@ public class SceneScriptManager { return config; } + public SceneGroup getCurrentGroup() { + return currentGroup; + } + public List<SceneBlock> getBlocks() { return blocks; } @@ -237,16 +241,16 @@ public class SceneScriptManager { for (SceneSuite suite : group.suites) { suite.sceneMonsters = new ArrayList<>(suite.monsters.size()); - for (int id : suite.monsters) { - try { - SceneMonster monster = (SceneMonster) map.get(id); - if (monster != null) { - suite.sceneMonsters.add(monster); + suite.monsters.forEach(id -> { + Object objEntry = map.get(id.intValue()); + if (objEntry instanceof Map.Entry<?,?> monsterEntry) { + Object monster = monsterEntry.getValue(); + if(monster instanceof SceneMonster sceneMonster){ + suite.sceneMonsters.add(sceneMonster); } - } catch (Exception e) { - continue; } - } + }); + suite.sceneGadgets = new ArrayList<>(suite.gadgets.size()); for (int id : suite.gadgets) { try { @@ -320,13 +324,15 @@ public class SceneScriptManager { } public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { - this.currentGroup = group; - this.monsterSceneLimit = 0; var suite = group.getSuiteByIndex(suiteIndex); if(suite == null){ return; } - suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); + if(suite.sceneMonsters.size() > 0){ + this.currentGroup = group; + this.monsterSceneLimit = 0; + suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); + } } public void spawnMonstersInGroup(SceneGroup group) { @@ -401,6 +407,7 @@ public class SceneScriptManager { spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); }else if(this.monsterAlive.get() == 0){ // spawn the last turn of monsters + //callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs()); while(!this.monsterOrders.isEmpty()){ spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); } diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 1b9badc11..1c4bbd0f2 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -118,7 +118,7 @@ public class ScriptLib { challengeIndex,groupId,ordersConfigId,tideCount,sceneLimit,param6); SceneGroup group = getSceneScriptManager().getGroupById(groupId); - + if (group == null || group.monsters == null) { return 1; } @@ -136,8 +136,7 @@ public class ScriptLib { if (group == null || group.monsters == null) { return 1; } - - // TODO just spawn all from group for now + this.getSceneScriptManager().spawnMonstersInGroup(group, suite); return 0; @@ -159,7 +158,13 @@ public class ScriptLib { if (group == null || group.monsters == null) { return 1; } - + + if(getSceneScriptManager().getScene().getChallenge() != null && + getSceneScriptManager().getScene().getChallenge().inProgress()) + { + return 0; + } + DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), group); challenge.setChallengeId(challengeId); challenge.setChallengeIndex(challengeIndex); @@ -249,7 +254,7 @@ public class ScriptLib { var1); return (int) getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityMonster) + .filter(e -> e instanceof EntityMonster && e.getGroupId() == getSceneScriptManager().getCurrentGroup().id) .count(); } public int SetMonsterBattleByGroup(int var1, int var2, int var3){ @@ -266,13 +271,11 @@ public class ScriptLib { return 0; } // 8-1 - public int GetGroupVariableValueByGroup(int var1, String var2, int var3){ - logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{},{}", - var1,var2,var3); + public int GetGroupVariableValueByGroup(String name, int groupId){ + logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{}", + name,groupId); - //TODO - - return getSceneScriptManager().getVariables().getOrDefault(var2, 0); + return getSceneScriptManager().getVariables().getOrDefault(name, 0); } public int SetIsAllowUseSkill(int canUse, int var2){ @@ -299,4 +302,11 @@ public class ScriptLib { return 0; } + public int SetGroupVariableValueByGroup(String key, int value, int groupId){ + logger.debug("[LUA] Call SetGroupVariableValueByGroup with {},{},{}", + key,value,groupId); + + return 0; + } + } diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 7ce8488ef..cb0e4965d 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -15,6 +15,7 @@ import emu.grasscutter.game.managers.InventoryManager; import emu.grasscutter.game.managers.MultiplayerManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.shop.ShopManager; +import emu.grasscutter.game.tower.TowerScheduleManager; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; @@ -54,6 +55,7 @@ public final class GameServer extends KcpServer { private final DropManager dropManager; private final CombineManger combineManger; + private final TowerScheduleManager towerScheduleManager; public GameServer() { this(new InetSocketAddress( @@ -82,7 +84,7 @@ public final class GameServer extends KcpServer { this.dropManager = new DropManager(this); this.expeditionManager = new ExpeditionManager(this); this.combineManger = new CombineManger(this); - + this.towerScheduleManager = new TowerScheduleManager(this); // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); } @@ -139,6 +141,10 @@ public final class GameServer extends KcpServer { return this.combineManger; } + public TowerScheduleManager getTowerScheduleManager() { + return towerScheduleManager; + } + public TaskMap getTaskMap() { return this.taskMap; } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java index 2a9ef2004..38462882f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTowerAllDataReq.java @@ -11,7 +11,10 @@ public class HandlerTowerAllDataReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - session.send(new PacketTowerAllDataRsp()); + session.send(new PacketTowerAllDataRsp( + session.getServer().getTowerScheduleManager(), + session.getPlayer().getTowerManager() + )); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java index 479029243..56d844d8d 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java @@ -46,7 +46,7 @@ public class PacketDungeonSettleNotify extends BasePacket { .setCount(1000) .build()) ; - if(nextFloorId > 0){ + if(nextFloorId > 0 && canJump){ towerLevelEndNotify.setNextFloorId(nextFloorId); } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java index d2d2376e6..654aa4a07 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerAllDataRsp.java @@ -1,37 +1,64 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.TowerFloorData; +import emu.grasscutter.game.tower.TowerManager; +import emu.grasscutter.game.tower.TowerScheduleManager; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.TowerAllDataRspOuterClass.TowerAllDataRsp; import emu.grasscutter.net.proto.TowerCurLevelRecordOuterClass.TowerCurLevelRecord; import emu.grasscutter.net.proto.TowerFloorRecordOuterClass.TowerFloorRecord; +import emu.grasscutter.net.proto.TowerLevelRecordOuterClass; +import emu.grasscutter.utils.DateHelper; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; public class PacketTowerAllDataRsp extends BasePacket { - public PacketTowerAllDataRsp() { + public PacketTowerAllDataRsp(TowerScheduleManager towerScheduleManager, TowerManager towerManager) { super(PacketOpcodes.TowerAllDataRsp); - var list = GameData.getTowerFloorDataMap().values().stream() - .map(TowerFloorData::getFloorId) - .map(id -> TowerFloorRecord.newBuilder().setFloorId(id).build()) - .collect(Collectors.toList()); + var recordList = towerManager.getRecordMap().values().stream() + .map(rec -> TowerFloorRecord.newBuilder() + .setFloorId(rec.getFloorId()) + .setFloorStarRewardProgress(rec.getFloorStarRewardProgress()) + .putAllPassedLevelMap(rec.getPassedLevelMap()) + .addAllPassedLevelRecordList(buildFromPassedLevelMap(rec.getPassedLevelMap())) + .build() + ) + .toList(); + + var openTimeMap = towerScheduleManager.getScheduleFloors().stream() + .collect(Collectors.toMap(x -> x, + y -> DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getScheduleStartTime())) + ); TowerAllDataRsp proto = TowerAllDataRsp.newBuilder() - .setTowerScheduleId(29) - .addAllTowerFloorRecordList(list) + .setTowerScheduleId(towerScheduleManager.getCurrentTowerScheduleData().getScheduleId()) + .addAllTowerFloorRecordList(recordList) .setCurLevelRecord(TowerCurLevelRecord.newBuilder().setIsEmpty(true)) - .setNextScheduleChangeTime(Integer.MAX_VALUE) - .putFloorOpenTimeMap(1024, 1630486800) - .putFloorOpenTimeMap(1025, 1630486800) - .putFloorOpenTimeMap(1026, 1630486800) - .putFloorOpenTimeMap(1027, 1630486800) - .setScheduleStartTime(1630486800) + .setScheduleStartTime(DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getScheduleStartTime())) + .setNextScheduleChangeTime(DateHelper.getUnixTime(towerScheduleManager.getTowerScheduleConfig() + .getNextScheduleChangeTime())) + .putAllFloorOpenTimeMap(openTimeMap) + .setIsFinishedEntranceFloor(towerManager.canEnterScheduleFloor()) .build(); this.setData(proto); } + + private List<TowerLevelRecordOuterClass.TowerLevelRecord> buildFromPassedLevelMap(Map<Integer, Integer> map){ + return map.entrySet().stream() + .map(item -> TowerLevelRecordOuterClass.TowerLevelRecord.newBuilder() + .setLevelId(item.getKey()) + .addAllSatisfiedCondList(IntStream.range(1, item.getValue() + 1).boxed().toList()) + .build()) + .toList(); + + } + } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java index c0ed414a8..5ab091901 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerFloorRecordChangeNotify.java @@ -8,13 +8,13 @@ import emu.grasscutter.net.proto.TowerLevelRecordOuterClass.TowerLevelRecord; public class PacketTowerFloorRecordChangeNotify extends BasePacket { - public PacketTowerFloorRecordChangeNotify(int floorId) { + public PacketTowerFloorRecordChangeNotify(int floorId, int stars, boolean canEnterScheduleFloor) { super(PacketOpcodes.TowerFloorRecordChangeNotify); TowerFloorRecordChangeNotify proto = TowerFloorRecordChangeNotify.newBuilder() .addTowerFloorRecordList(TowerFloorRecord.newBuilder() .setFloorId(floorId) - .setFloorStarRewardProgress(3) + .setFloorStarRewardProgress(stars) .addPassedLevelRecordList(TowerLevelRecord.newBuilder() .setLevelId(1) .addSatisfiedCondList(1) @@ -22,7 +22,7 @@ public class PacketTowerFloorRecordChangeNotify extends BasePacket { .addSatisfiedCondList(3) .build()) .build()) - .setIsFinishedEntranceFloor(true) + .setIsFinishedEntranceFloor(canEnterScheduleFloor) .build(); this.setData(proto); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java new file mode 100644 index 000000000..c2c301e4e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerLevelStarCondNotify.java @@ -0,0 +1,32 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerLevelStarCondDataOuterClass.TowerLevelStarCondData; +import emu.grasscutter.net.proto.TowerLevelStarCondNotifyOuterClass.TowerLevelStarCondNotify; + +public class PacketTowerLevelStarCondNotify extends BasePacket { + + public PacketTowerLevelStarCondNotify(int floorId, int levelIndex) { + super(PacketOpcodes.TowerLevelStarCondNotify); + + TowerLevelStarCondNotify proto = TowerLevelStarCondNotify.newBuilder() + .setFloorId(floorId) + .setLevelIndex(levelIndex) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(1) + .build() + ) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(2) + .build() + ) + .addCondDataList(TowerLevelStarCondData.newBuilder() + .setCondValue(3) + .build() + ) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/utils/DateHelper.java b/src/main/java/emu/grasscutter/utils/DateHelper.java index 7005d9457..1f1393760 100644 --- a/src/main/java/emu/grasscutter/utils/DateHelper.java +++ b/src/main/java/emu/grasscutter/utils/DateHelper.java @@ -1,7 +1,7 @@ package emu.grasscutter.utils; -import java.util.Date; import java.util.Calendar; +import java.util.Date; public final class DateHelper { public static Date onlyYearMonthDay(Date now) { @@ -13,4 +13,8 @@ public final class DateHelper { calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } + + public static int getUnixTime(Date localDateTime){ + return (int)(localDateTime.getTime() / 1000L); + } } diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 9a7485608..4ec17a214 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -312,6 +312,9 @@ "success": "Teleported %s to %s, %s, %s in scene %s", "description": "Change the player's position." }, + "tower": { + "unlock_done": "Abyss Corridor's Floors are all unlocked now." + }, "weather": { "usage": "Usage: weather <weatherId> [climateId]", "success": "Changed weather to %s with climate %s", From 6ce96d1c267c5de28a1a940478876cfd5e54b625 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sun, 8 May 2022 21:07:37 +0800 Subject: [PATCH 210/434] feature(task): Implement pause, resume and cancel Use as `pauseTask(taskName)`. They return boolean values to tell the developer if a timed task can be paused/resumed/cancelled properly. A little bit of testing shows that pausing and then resuming may execute the task multiple times. --- src/main/java/emu/grasscutter/task/Task.java | 2 -- .../java/emu/grasscutter/task/TaskMap.java | 36 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/task/Task.java b/src/main/java/emu/grasscutter/task/Task.java index 1f35d16ce..2c930c0e3 100644 --- a/src/main/java/emu/grasscutter/task/Task.java +++ b/src/main/java/emu/grasscutter/task/Task.java @@ -1,7 +1,5 @@ package emu.grasscutter.task; -import org.quartz.JobDataMap; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/emu/grasscutter/task/TaskMap.java b/src/main/java/emu/grasscutter/task/TaskMap.java index fe067e795..a4b2ff02f 100644 --- a/src/main/java/emu/grasscutter/task/TaskMap.java +++ b/src/main/java/emu/grasscutter/task/TaskMap.java @@ -67,6 +67,40 @@ public final class TaskMap { return this; } + public boolean pauseTask(String taskName) { + try { + Scheduler scheduler = schedulerFactory.getScheduler(); + scheduler.pauseJob(new JobKey(taskName)); + } catch (SchedulerException e) { + e.printStackTrace(); + return false; + } + return true; + } + + public boolean resumeTask(String taskName) { + try { + Scheduler scheduler = schedulerFactory.getScheduler(); + scheduler.resumeJob(new JobKey(taskName)); + } catch (SchedulerException e) { + e.printStackTrace(); + return false; + } + return true; + } + + public boolean cancelTask(String taskName) { + Task task = this.annotations.get(taskName); + if (task == null) return false; + try { + this.unregisterTask(this.tasks.get(taskName)); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + public TaskMap registerTask(String taskName, TaskHandler task) { Task annotation = task.getClass().getAnnotation(Task.class); this.annotations.put(taskName, annotation); @@ -116,7 +150,7 @@ public final class TaskMap { classes.forEach(annotated -> { try { Task taskData = annotated.getAnnotation(Task.class); - Object object = annotated.newInstance(); + Object object = annotated.getDeclaredConstructor().newInstance(); if (object instanceof TaskHandler) { this.registerTask(taskData.taskName(), (TaskHandler) object); if (taskData.executeImmediatelyAfterReset()) { From bcd8d3908474b09b62074c673d8f7c839b2f36d1 Mon Sep 17 00:00:00 2001 From: memetrollsXD <memetrollsxd@gmail.com> Date: Sun, 8 May 2022 14:26:55 +0200 Subject: [PATCH 211/434] Change LICENSE to GNU Affero With these changes, commercial use is still allowed, but are required to disclose the source, changes, and copyright. Meaning that people won't be unknowingly ripped off, and would be paying knowing that it is an open-source project with possible changes. tl;drLegal link: https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0) --- LICENSE | 798 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 629 insertions(+), 169 deletions(-) diff --git a/LICENSE b/LICENSE index b09cd7856..0ad25db4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. - 1. Definitions. + Preamble - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + The precise terms and conditions for copying, distribution and +modification follow. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + TERMS AND CONDITIONS - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + 0. Definitions. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "This License" refers to version 3 of the GNU Affero General Public License. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + A "covered work" means either the unmodified Program or a work based +on the Program. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + 1. Source Code. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. - END OF TERMS AND CONDITIONS + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. - APPENDIX: How to apply the Apache License to your work. + The Corresponding Source for a work in source code form is that +same work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + 2. Basic Permissions. - Copyright [yyyy] [name of copyright owner] + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. - http://www.apache.org/licenses/LICENSE-2.0 + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. From e56cd8385a0cfe5ffd3456c82c32c44fdc179585 Mon Sep 17 00:00:00 2001 From: memetrollsXD <memetrollsxd@gmail.com> Date: Sun, 8 May 2022 17:09:23 +0200 Subject: [PATCH 212/434] Update com.google.code.gson 2.8.8 -> 2.9.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eefef5b48..dd8fa9fa0 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' From 1234a88c6784f7f99c1b98689215eb2f32441ec7 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Sun, 8 May 2022 11:53:37 -0400 Subject: [PATCH 213/434] Fallback to the fallback fallback --- src/main/java/emu/grasscutter/utils/Language.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 0af77adc1..d38907354 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -46,12 +46,15 @@ public final class Language { private Language(String fileName, String fallback) { @Nullable JsonObject languageData = null; + InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); + if(file == null) // Provided fallback language. + file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); + if(file == null) // Fallback the fallback language. + file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); + if(file == null) + throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files."); + try { - InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); - if(file == null) { - file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); - } - languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); } catch (Exception exception) { Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception); From 392ce26e32209dc000240134feaae1be8b2da067 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Sun, 8 May 2022 11:58:32 -0400 Subject: [PATCH 214/434] Add warning for language fallback --- src/main/java/emu/grasscutter/Config.java | 1 - src/main/java/emu/grasscutter/utils/Language.java | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 3982fc46b..6473e2846 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -16,7 +16,6 @@ public final class Config { public String KEY_FOLDER = "./keys/"; public String SCRIPTS_FOLDER = "./resources/Scripts/"; public String PLUGINS_FOLDER = "./plugins/"; - public String LANGUAGE_FOLDER = "./languages/"; public ServerDebugMode DebugMode = ServerDebugMode.NONE; // ALL, MISSING, NONE public ServerRunMode RunMode = ServerRunMode.HYBRID; // HYBRID, DISPATCH_ONLY, GAME_ONLY diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index d38907354..70e32e658 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -19,7 +19,7 @@ public final class Language { * @return A language instance. */ public static Language getLanguage(String langCode) { - return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag()); + return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json"); } /** @@ -47,10 +47,14 @@ public final class Language { @Nullable JsonObject languageData = null; InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); - if(file == null) // Provided fallback language. + if (file == null) { // Provided fallback language. file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); - if(file == null) // Fallback the fallback language. + Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); + } + if(file == null) { // Fallback the fallback language. file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); + Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); + } if(file == null) throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files."); From 14bf96e907d6393119f7050d64ed063e5bfa2617 Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Sun, 8 May 2022 08:44:05 -0700 Subject: [PATCH 215/434] Added to ability to specify main and substats for /giveart via names instead of IDs. --- .../command/commands/GiveArtifactCommand.java | 116 +++++++++++++++++- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index b87642bb2..3b8279d98 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -9,28 +9,109 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.inventory.EquipType; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import static java.util.Map.entry; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart", description = "commands.giveArtifact.description") +@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", description = "Gives the player a specified artifact", aliases = {"gart"}, permission = "player.giveart") public final class GiveArtifactCommand implements CommandHandler { + private static final Map<String, Map<EquipType, Integer>> mainPropMap = Map.ofEntries( + entry("hp", Map.ofEntries(entry(EquipType.EQUIP_BRACER, 14001))), + entry("hp%", Map.ofEntries(entry(EquipType.EQUIP_SHOES, 10980), entry(EquipType.EQUIP_RING, 50980), entry(EquipType.EQUIP_DRESS, 30980))), + entry("atk", Map.ofEntries(entry(EquipType.EQUIP_NECKLACE, 12001))), + entry("atk%", Map.ofEntries(entry(EquipType.EQUIP_SHOES, 10990), entry(EquipType.EQUIP_RING, 50990), entry(EquipType.EQUIP_DRESS, 30990))), + entry("def%", Map.ofEntries(entry(EquipType.EQUIP_SHOES, 10970), entry(EquipType.EQUIP_RING, 50970), entry(EquipType.EQUIP_DRESS, 30970))), + entry("er", Map.ofEntries(entry(EquipType.EQUIP_SHOES, 10960))), + entry("em", Map.ofEntries(entry(EquipType.EQUIP_SHOES, 10950), entry(EquipType.EQUIP_RING, 50880), entry(EquipType.EQUIP_DRESS, 30930))), + entry("hb", Map.ofEntries(entry(EquipType.EQUIP_DRESS, 30940))), + entry("cdmg", Map.ofEntries(entry(EquipType.EQUIP_DRESS, 30950))), + entry("cr", Map.ofEntries(entry(EquipType.EQUIP_DRESS, 30960))), + entry("phys%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50890))), + entry("dendro%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50900))), + entry("geo%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50910))), + entry("anemo%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50920))), + entry("hydro%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50930))), + entry("cryo%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50940))), + entry("electro%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50950))), + entry("pyro%", Map.ofEntries(entry(EquipType.EQUIP_RING, 50960))) + ); + private static final Map<String, String> appendPropMap = Map.ofEntries( + entry("hp", "0102"), + entry("hp%", "0103"), + entry("atk", "0105"), + entry("atk%", "0106"), + entry("def", "0108"), + entry("def%", "0109"), + entry("er", "0123"), + entry("em", "0124"), + entry("cr", "0120"), + entry("cdmg", "0122") + ); + + private int getAppendPropId(String substatText, ItemData itemData) { + int res; + + // If the given substat text is an integer, we just use that + // as the append prop ID. + try { + res = Integer.parseInt(substatText); + return res; + } + catch (NumberFormatException ignores) { + // No need to handle this here. We just continue with the + // possibility of the argument being a substat string. + } + + // If the argument was not an integer, we try to determine + // the append prop ID from the given text + artifact information. + // A substat string has the format `substat_tier`. + String[] substatArgs = substatText.split("_"); + if (substatArgs.length != 2) { + throw new IllegalArgumentException(); + } + + String substatType = substatArgs[0]; + int substatTier = Integer.parseInt(substatArgs[1]); + + // Check if the specified tier is legal for the artifact rarity. + if (substatTier < 1 || substatTier > 4) { + throw new IllegalArgumentException(); + } + if (itemData.getRankLevel() == 1 && substatTier > 2) { + throw new IllegalArgumentException(); + } + if (itemData.getRankLevel() == 2 && substatTier > 3) { + throw new IllegalArgumentException(); + } + + // Check if the given substat type string is a legal stat. + if (!appendPropMap.containsKey(substatType)) { + throw new IllegalArgumentException(); + } + + // Build the append prop ID. + return Integer.parseInt(Integer.toString(itemData.getRankLevel()) + appendPropMap.get(substatType) + Integer.toString(substatTier)); + } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { + // Sanity checks if (targetPlayer == null) { CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); return; } - if (args.size() < 2) { CommandHandler.sendMessage(sender, translate("commands.giveArtifact.usage")); return; } + // Get the artifact piece ID from the arguments. int itemId; try { itemId = Integer.parseInt(args.remove(0)); @@ -38,20 +119,35 @@ public final class GiveArtifactCommand implements CommandHandler { CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); return; } + ItemData itemData = GameData.getItemDataMap().get(itemId); if (itemData.getItemType() != ItemType.ITEM_RELIQUARY) { CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); return; } + // Get the main stat from the arguments. + // If the given argument is an integer, we use that. + // If not, we check if the argument string is in the main prop map. + String mainPropIdString = args.remove(0); int mainPropId; + try { - mainPropId = Integer.parseInt(args.remove(0)); + mainPropId = Integer.parseInt(mainPropIdString); } catch (NumberFormatException ignored) { + mainPropId = -1; + } + + if (mainPropMap.containsKey(mainPropIdString) && mainPropMap.get(mainPropIdString).containsKey(itemData.getEquipType())) { + mainPropId = mainPropMap.get(mainPropIdString).get(itemData.getEquipType()); + } + + if (mainPropId == -1) { CommandHandler.sendMessage(sender, translate("commands.generic.execution.argument_error")); return; } + // Get the level from the arguments. int level = 1; try { int last = Integer.parseInt(args.get(args.size()-1)); @@ -62,9 +158,13 @@ public final class GiveArtifactCommand implements CommandHandler { } catch (NumberFormatException ignored) { // Could be a stat,times string so no need to panic } - List<Integer> appendPropIdList = new ArrayList<>(); + // Get substats. + ArrayList<Integer> appendPropIdList = new ArrayList<>(); try { + // Every remaining argument is a substat. args.forEach(it -> { + // The substat syntax permits specifying a number of rolls for the given + // substat. Split the string into stat and number if that is the case here. String[] arr; int n = 1; if ((arr = it.split(",")).length == 2) { @@ -74,13 +174,19 @@ public final class GiveArtifactCommand implements CommandHandler { n = 200; } } - appendPropIdList.addAll(Collections.nCopies(n, Integer.parseInt(it))); + + // Determine the substat ID. + int appendPropId = getAppendPropId(it, itemData); + + // Add the current substat. + appendPropIdList.addAll(Collections.nCopies(n, appendPropId)); }); } catch (Exception ignored) { CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); return; } + // Create item for the artifact. GameItem item = new GameItem(itemData); item.setLevel(level); item.setMainPropId(mainPropId); From e8e48600aeba2e209f35b070b9aba2e376be55e3 Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Sun, 8 May 2022 09:09:09 -0700 Subject: [PATCH 216/434] Fix invalid translation key. --- .../emu/grasscutter/command/commands/GiveArtifactCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 3b8279d98..23efd0d79 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -143,7 +143,7 @@ public final class GiveArtifactCommand implements CommandHandler { } if (mainPropId == -1) { - CommandHandler.sendMessage(sender, translate("commands.generic.execution.argument_error")); + CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); return; } From 2a12ed5694ca474782b4fde96b9e637c24bf23b4 Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Sun, 8 May 2022 09:18:53 -0700 Subject: [PATCH 217/434] Fix incorrect @Command annotation. --- .../emu/grasscutter/command/commands/GiveArtifactCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 23efd0d79..6a0e3ce24 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -19,7 +19,7 @@ import static java.util.Map.entry; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", description = "Gives the player a specified artifact", aliases = {"gart"}, permission = "player.giveart") +@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart", description = "commands.giveArtifact.description") public final class GiveArtifactCommand implements CommandHandler { private static final Map<String, Map<EquipType, Integer>> mainPropMap = Map.ofEntries( entry("hp", Map.ofEntries(entry(EquipType.EQUIP_BRACER, 14001))), From 8c7a46e8ef46749645c702529786e8ec90b59c2a Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Sun, 8 May 2022 13:25:47 -0700 Subject: [PATCH 218/434] -Make the _tier suffix optional. --- .../command/commands/GiveArtifactCommand.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 6a0e3ce24..cf479ed57 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -70,15 +70,27 @@ public final class GiveArtifactCommand implements CommandHandler { // If the argument was not an integer, we try to determine // the append prop ID from the given text + artifact information. - // A substat string has the format `substat_tier`. + // A substat string has the format `substat_tier`, with the + // `_tier` part being optional. String[] substatArgs = substatText.split("_"); - if (substatArgs.length != 2) { + String substatType; + int substatTier; + + if (substatArgs.length == 1) { + substatType = substatArgs[0]; + substatTier = + itemData.getRankLevel() == 1 ? 2 + : itemData.getRankLevel() == 2 ? 3 + : 4; + } + else if (substatArgs.length == 2) { + substatType = substatArgs[0]; + substatTier = Integer.parseInt(substatArgs[1]); + } + else { throw new IllegalArgumentException(); } - String substatType = substatArgs[0]; - int substatTier = Integer.parseInt(substatArgs[1]); - // Check if the specified tier is legal for the artifact rarity. if (substatTier < 1 || substatTier > 4) { throw new IllegalArgumentException(); From f0ff323b7bfdb070e98f75ee474f81287a4bb67f Mon Sep 17 00:00:00 2001 From: Shirakami Ling <i@foxex.cn> Date: Mon, 9 May 2022 06:57:39 +0800 Subject: [PATCH 219/434] Change the license type in zh-CN.json. --- src/main/resources/languages/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 4e4929aee..89d1d6a90 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -36,7 +36,7 @@ } }, "status": { - "free_software": "Grasscutter 是免费开源软件,遵循Apache-2.0 license。如果您是付费购买的,那您已经被骗了。项目地址:Github:https://github.com/Grasscutters/Grasscutter", + "free_software": "Grasscutter 是免费开源软件,遵循AGPL-3.0 license。如果您是付费购买的,那您已经被骗了。项目地址:Github:https://github.com/Grasscutters/Grasscutter", "starting": "正在启动 Grasscutter...", "shutdown": "正在关闭...", "done": "加载完成!输入 \"help\" 查看命令列表", From d00465125d831aa0ccc6c9161e730e6fc7dbb2c9 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Mon, 9 May 2022 15:39:49 +0800 Subject: [PATCH 220/434] Support Team Toggle in Tower & Refactor MonsterTide --- proto/TowerMiddleLevelChangeTeamNotify.proto | 14 ++ .../dungeons/TowerDungeonSettleListener.java | 4 + .../game/entity/EntityMonster.java | 4 +- .../grasscutter/game/tower/TowerManager.java | 11 +- .../game/tower/TowerScheduleManager.java | 5 +- .../scripts/SceneScriptManager.java | 182 ++++++++---------- .../emu/grasscutter/scripts/ScriptLib.java | 50 ++++- .../scripts/data/SceneTrigger.java | 14 ++ .../service/ScriptMonsterSpawnService.java | 73 +++++++ .../service/ScriptMonsterTideService.java | 74 +++++++ ...acketTowerMiddleLevelChangeTeamNotify.java | 18 ++ 11 files changed, 334 insertions(+), 115 deletions(-) create mode 100644 proto/TowerMiddleLevelChangeTeamNotify.proto create mode 100644 src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java create mode 100644 src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTowerMiddleLevelChangeTeamNotify.java diff --git a/proto/TowerMiddleLevelChangeTeamNotify.proto b/proto/TowerMiddleLevelChangeTeamNotify.proto new file mode 100644 index 000000000..35f2685ee --- /dev/null +++ b/proto/TowerMiddleLevelChangeTeamNotify.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message TowerMiddleLevelChangeTeamNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 2417; + } + +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java index c480d047f..3f212ce4a 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -9,6 +9,10 @@ public class TowerDungeonSettleListener implements DungeonSettleListener { @Override public void onDungeonSettle(Scene scene) { + if(scene.getScriptManager().getVariables().containsKey("stage") + && scene.getScriptManager().getVariables().get("stage") == 1){ + return; + } scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); var towerManager = scene.getPlayers().get(0).getTowerManager(); diff --git a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java index 0ae6f356b..f8a96a808 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java @@ -117,7 +117,9 @@ public class EntityMonster extends GameEntity { this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); } if (getScene().getScriptManager().isInit() && this.getGroupId() > 0) { - getScene().getScriptManager().onMonsterDie(); + if(getScene().getScriptManager().getScriptMonsterSpawnService() != null){ + getScene().getScriptManager().getScriptMonsterSpawnService().onMonsterDead(this); + } getScene().getScriptManager().callEvent(EventType.EVENT_ANY_MONSTER_DIE, null); } if (getScene().getChallenge() != null && getScene().getChallenge().getGroup().id == this.getGroupId()) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index 9346ffead..cac848ea6 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -7,10 +7,7 @@ import emu.grasscutter.data.def.TowerLevelData; import emu.grasscutter.game.dungeons.DungeonSettleListener; import emu.grasscutter.game.dungeons.TowerDungeonSettleListener; import emu.grasscutter.game.player.Player; -import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; -import emu.grasscutter.server.packet.send.PacketTowerCurLevelRecordChangeNotify; -import emu.grasscutter.server.packet.send.PacketTowerEnterLevelRsp; -import emu.grasscutter.server.packet.send.PacketTowerLevelStarCondNotify; +import emu.grasscutter.server.packet.send.*; import java.util.HashMap; import java.util.List; @@ -152,4 +149,10 @@ public class TowerManager { return recordMap.get(player.getServer().getTowerScheduleManager().getLastEntranceFloor()) .getStarCount() >= 6; } + + public void mirrorTeamSetUp(int teamId) { + // use team user choose + player.getTeamManager().useTemporaryTeam(teamId); + player.sendPacket(new PacketTowerMiddleLevelChangeTeamNotify()); + } } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java index 33f5c158d..952acd806 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java @@ -49,6 +49,7 @@ public class TowerScheduleManager { public int getNextFloorId(int floorId){ var entranceFloors = getCurrentTowerScheduleData().getEntranceFloorId(); + var scheduleFloors = getScheduleFloors(); var nextId = 0; // find in entrance floors first for(int i=0;i<entranceFloors.size()-1;i++){ @@ -56,10 +57,12 @@ public class TowerScheduleManager { nextId = entranceFloors.get(i+1); } } + if(floorId == entranceFloors.get(entranceFloors.size()-1)){ + nextId = scheduleFloors.get(0); + } if(nextId != 0){ return nextId; } - var scheduleFloors = getScheduleFloors(); // find in schedule floors for(int i=0;i<scheduleFloors.size()-1;i++){ if(floorId == scheduleFloors.get(i)){ diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 611a9aa17..bc0e896f5 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -1,23 +1,20 @@ package emu.grasscutter.scripts; import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptException; +import emu.grasscutter.scripts.service.ScriptMonsterSpawnService; +import emu.grasscutter.scripts.service.ScriptMonsterTideService; +import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaValue; import org.luaj.vm2.lib.jse.CoerceJavaToLua; import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.MonsterData; -import emu.grasscutter.data.def.WorldLevelData; import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneBlock; @@ -39,28 +36,36 @@ public class SceneScriptManager { private final ScriptLib scriptLib; private final LuaValue scriptLibLua; private final Map<String, Integer> variables; - private Bindings bindings; private SceneConfig config; private List<SceneBlock> blocks; private boolean isInit; - - private final Int2ObjectOpenHashMap<Set<SceneTrigger>> triggers; + /** + * SceneTrigger Set + */ + private final Map<String, SceneTrigger> triggers; + /** + * current triggers controlled by RefreshGroup + */ + private final Int2ObjectOpenHashMap<Set<SceneTrigger>> currentTriggers; private final Int2ObjectOpenHashMap<SceneRegion> regions; + private Map<Integer,SceneGroup> sceneGroups; private SceneGroup currentGroup; - private AtomicInteger monsterAlive; - private AtomicInteger monsterTideCount; - private int monsterSceneLimit; - private ConcurrentLinkedQueue<Integer> monsterOrders; + private ScriptMonsterTideService scriptMonsterTideService; + private ScriptMonsterSpawnService scriptMonsterSpawnService; public SceneScriptManager(Scene scene) { this.scene = scene; this.scriptLib = new ScriptLib(this); this.scriptLibLua = CoerceJavaToLua.coerce(this.scriptLib); - this.triggers = new Int2ObjectOpenHashMap<>(); + this.triggers = new HashMap<>(); + this.currentTriggers = new Int2ObjectOpenHashMap<>(); + this.regions = new Int2ObjectOpenHashMap<>(); this.variables = new HashMap<>(); - + this.sceneGroups = new HashMap<>(); + this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); + // TEMPORARY if (this.getScene().getId() < 10) { return; @@ -103,17 +108,35 @@ public class SceneScriptManager { } public Set<SceneTrigger> getTriggersByEvent(int eventId) { - return triggers.computeIfAbsent(eventId, e -> new HashSet<>()); + return currentTriggers.computeIfAbsent(eventId, e -> new HashSet<>()); } - public void registerTrigger(SceneTrigger trigger) { + this.triggers.put(trigger.name, trigger); getTriggersByEvent(trigger.event).add(trigger); } public void deregisterTrigger(SceneTrigger trigger) { + this.triggers.remove(trigger.name); getTriggersByEvent(trigger.event).remove(trigger); } - + public void resetTriggers(List<String> triggerNames) { + for(var name : triggerNames){ + var instance = triggers.get(name); + this.currentTriggers.get(instance.event).clear(); + this.currentTriggers.get(instance.event).add(instance); + } + } + public void refreshGroup(SceneGroup group, int suiteIndex){ + var suite = group.getSuiteByIndex(suiteIndex); + if(suite == null){ + return; + } + if(suite.triggers.size() > 0){ + resetTriggers(suite.triggers); + } + spawnMonstersInGroup(group, suite); + spawnGadgetsInGroup(group, suite); + } public SceneRegion getRegionById(int id) { return regions.get(id); } @@ -263,6 +286,7 @@ public class SceneScriptManager { } } } + this.sceneGroups.put(group.id, group); } catch (ScriptException e) { Grasscutter.getLogger().error("Error loading group " + group.id + " in scene " + getScene().getId(), e); } @@ -322,96 +346,36 @@ public class SceneScriptManager { this.callEvent(EventType.EVENT_GADGET_CREATE, new ScriptArgs(entity.getConfigId())); } } - + public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { var suite = group.getSuiteByIndex(suiteIndex); if(suite == null){ return; } - if(suite.sceneMonsters.size() > 0){ - this.currentGroup = group; - this.monsterSceneLimit = 0; - suite.sceneMonsters.forEach(mob -> spawnMonstersInGroup(group, mob)); + spawnMonstersInGroup(group, suite); + } + public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { + if(suite == null || suite.sceneMonsters.size() <= 0){ + return; } + this.currentGroup = group; + suite.sceneMonsters.forEach(mob -> this.scriptMonsterSpawnService.spawnMonster(group.id, mob)); } public void spawnMonstersInGroup(SceneGroup group) { this.currentGroup = group; - this.monsterSceneLimit = 0; - group.monsters.values().forEach(mob -> spawnMonstersInGroup(group, mob)); + group.monsters.values().forEach(mob -> this.scriptMonsterSpawnService.spawnMonster(group.id, mob)); } - public void spawnMonstersInGroup(SceneGroup group,Integer[] ordersConfigId, int tideCount, int sceneLimit) { + + public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { this.currentGroup = group; - this.monsterSceneLimit = sceneLimit; - this.monsterTideCount = new AtomicInteger(tideCount); - this.monsterAlive = new AtomicInteger(0); - this.monsterOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); + this.scriptMonsterTideService = + new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId); - // add the last turn - group.monsters.keySet().stream() - .filter(i -> !this.monsterOrders.contains(i)) - .forEach(this.monsterOrders::add); - for (int i = 0; i < sceneLimit; i++) { - spawnMonstersInGroup(group, group.monsters.get(this.monsterOrders.poll())); - } } - public void spawnMonstersInGroup(SceneGroup group, SceneMonster monster) { - if(monster == null){ - return; - } - if(this.monsterSceneLimit > 0){ - this.monsterTideCount.decrementAndGet(); - this.monsterAlive.incrementAndGet(); - } - - MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); - - if (data == null) { - return; - } - - // Calculate level - int level = monster.level; - - if (getScene().getDungeonData() != null) { - level = getScene().getDungeonData().getShowLevel(); - } else if (getScene().getWorld().getWorldLevel() > 0) { - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); - - if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); - } - } - - // Spawn mob - EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); - entity.getRotation().set(monster.rot); - entity.setGroupId(group.id); - entity.setConfigId(monster.config_id); - - getScene().addEntity(entity); - - callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); - } - - public void onMonsterDie(){ - if(this.monsterSceneLimit <= 0){ - return; - } - if(this.monsterAlive.decrementAndGet() >= this.monsterSceneLimit) { - // maybe not happen - return; - } - if(this.monsterTideCount.get() > 0){ - // add more - spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); - }else if(this.monsterAlive.get() == 0){ - // spawn the last turn of monsters - //callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs()); - while(!this.monsterOrders.isEmpty()){ - spawnMonstersInGroup(this.currentGroup, this.currentGroup.monsters.get(this.monsterOrders.poll())); - } - } + public void spawnMonstersByConfigId(int configId, int delayTime) { + // TODO delay + this.scriptMonsterSpawnService.spawnMonster(this.currentGroup.id, this.currentGroup.monsters.get(configId)); } // Events @@ -432,17 +396,35 @@ public class SceneScriptManager { args = CoerceJavaToLua.coerce(params); } - ret = condition.call(this.getScriptLibLua(), args); + ret = safetyCall(trigger.condition, condition, args); } - if (ret.checkboolean() == true) { + if (ret.isboolean() && ret.checkboolean()) { LuaValue action = (LuaValue) this.getBindings().get(trigger.action); - action.call(this.getScriptLibLua(), LuaValue.NIL); + var arg = new ScriptArgs(); + arg.param2 = 100; + var args = CoerceJavaToLua.coerce(arg); + safetyCall(trigger.action, action, args); } + //TODO some ret may not bool } } -// public LuaValue safetyCall(){ -// -// } + public LuaValue safetyCall(String name, LuaValue func, LuaValue args){ + try{ + return func.call(this.getScriptLibLua(), args); + }catch (LuaError error){ + ScriptLib.logger.error("[LUA] call trigger failed {},{}",name,args,error); + return LuaValue.valueOf(-1); + } + } + + public ScriptMonsterTideService getScriptMonsterTideService() { + return scriptMonsterTideService; + } + + public ScriptMonsterSpawnService getScriptMonsterSpawnService() { + return scriptMonsterSpawnService; + } + } diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 1c4bbd0f2..0d686dd5a 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -28,7 +28,17 @@ public class ScriptLib { public SceneScriptManager getSceneScriptManager() { return sceneScriptManager; } - + + private String printTable(LuaTable table){ + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for(var meta : table.keys()){ + sb.append(meta).append(":").append(table.get(meta)).append(","); + } + sb.append("}"); + return sb.toString(); + } + public int SetGadgetStateByConfigId(int configId, int gadgetState) { logger.debug("[LUA] Call SetGadgetStateByConfigId with {},{}", configId,gadgetState); @@ -123,7 +133,7 @@ public class ScriptLib { return 1; } - this.getSceneScriptManager().spawnMonstersInGroup(group, ordersConfigId, tideCount, sceneLimit); + this.getSceneScriptManager().startMonsterTideInGroup(group, ordersConfigId, tideCount, sceneLimit); return 0; } @@ -204,10 +214,13 @@ public class ScriptLib { getSceneScriptManager().getVariables().put(var, getSceneScriptManager().getVariables().get(var) + value); return LuaValue.ZERO; } - + + /** + * Set the actions and triggers to designated group + */ public int RefreshGroup(LuaTable table) { logger.debug("[LUA] Call RefreshGroup with {}", - table); + printTable(table)); // Kill and Respawn? int groupId = table.get("group_id").toint(); int suite = table.get("suite").toint(); @@ -218,8 +231,7 @@ public class ScriptLib { return 1; } - this.getSceneScriptManager().spawnMonstersInGroup(group, suite); - this.getSceneScriptManager().spawnGadgetsInGroup(group, suite); + getSceneScriptManager().refreshGroup(group, suite); return 0; } @@ -260,7 +272,7 @@ public class ScriptLib { public int SetMonsterBattleByGroup(int var1, int var2, int var3){ logger.debug("[LUA] Call SetMonsterBattleByGroup with {},{},{}", var1,var2,var3); - + // TODO return 0; } @@ -270,7 +282,7 @@ public class ScriptLib { return 0; } - // 8-1 + public int GetGroupVariableValueByGroup(String name, int groupId){ logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{}", name,groupId); @@ -288,7 +300,7 @@ public class ScriptLib { public int KillEntityByConfigId(LuaTable table){ logger.debug("[LUA] Call KillEntityByConfigId with {}", - table); + printTable(table)); var configId = table.get("config_id"); if(configId == LuaValue.NIL){ return 1; @@ -306,6 +318,26 @@ public class ScriptLib { logger.debug("[LUA] Call SetGroupVariableValueByGroup with {},{},{}", key,value,groupId); + getSceneScriptManager().getVariables().put(key, value); + return 0; + } + + public int CreateMonster(LuaTable table){ + logger.debug("[LUA] Call CreateMonster with {}", + printTable(table)); + var configId = table.get("config_id").toint(); + var delayTime = table.get("delay_time").toint(); + + getSceneScriptManager().spawnMonstersByConfigId(configId, delayTime); + return 0; + } + + public int TowerMirrorTeamSetUp(int team, int var1) { + logger.debug("[LUA] Call TowerMirrorTeamSetUp with {},{}", + team,var1); + + getSceneScriptManager().getScene().getPlayers().get(0).getTowerManager().mirrorTeamSetUp(team-1); + return 0; } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java index a1603b1e6..a627f67c4 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java @@ -7,4 +7,18 @@ public class SceneTrigger { public String source; public String condition; public String action; + + @Override + public boolean equals(Object obj) { + if(obj instanceof SceneTrigger sceneTrigger){ + return this.name.equals(sceneTrigger.name); + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java new file mode 100644 index 000000000..dda0d4732 --- /dev/null +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java @@ -0,0 +1,73 @@ +package emu.grasscutter.scripts.service; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.MonsterData; +import emu.grasscutter.data.def.WorldLevelData; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.scripts.SceneScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneMonster; +import emu.grasscutter.scripts.data.ScriptArgs; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class ScriptMonsterSpawnService { + + private final SceneScriptManager sceneScriptManager; + private final List<Consumer<EntityMonster>> onMonsterCreatedListener = new ArrayList<>(); + + private final List<Consumer<EntityMonster>> onMonsterDeadListener = new ArrayList<>(); + + public ScriptMonsterSpawnService(SceneScriptManager sceneScriptManager){ + this.sceneScriptManager = sceneScriptManager; + } + + public void addMonsterCreatedListener(Consumer<EntityMonster> consumer){ + onMonsterCreatedListener.add(consumer); + } + public void addMonsterDeadListener(Consumer<EntityMonster> consumer){ + onMonsterCreatedListener.add(consumer); + } + + public void onMonsterDead(EntityMonster entityMonster){ + onMonsterCreatedListener.stream().forEach(l -> l.accept(entityMonster)); + } + public void spawnMonster(int groupId, SceneMonster monster) { + if(monster == null){ + return; + } + + MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); + + if (data == null) { + return; + } + + // Calculate level + int level = monster.level; + + if (sceneScriptManager.getScene().getDungeonData() != null) { + level = sceneScriptManager.getScene().getDungeonData().getShowLevel(); + } else if (sceneScriptManager.getScene().getWorld().getWorldLevel() > 0) { + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(sceneScriptManager.getScene().getWorld().getWorldLevel()); + + if (worldLevelData != null) { + level = worldLevelData.getMonsterLevel(); + } + } + + // Spawn mob + EntityMonster entity = new EntityMonster(sceneScriptManager.getScene(), data, monster.pos, level); + entity.getRotation().set(monster.rot); + entity.setGroupId(groupId); + entity.setConfigId(monster.config_id); + + onMonsterCreatedListener.forEach(action -> action.accept(entity)); + + sceneScriptManager.getScene().addEntity(entity); + + sceneScriptManager.callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(entity.getConfigId())); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java new file mode 100644 index 000000000..117297f15 --- /dev/null +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -0,0 +1,74 @@ +package emu.grasscutter.scripts.service; + +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.scripts.SceneScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneMonster; +import emu.grasscutter.scripts.data.ScriptArgs; + +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +public class ScriptMonsterTideService { + private final SceneScriptManager sceneScriptManager; + private final SceneGroup currentGroup; + private final AtomicInteger monsterAlive; + private final AtomicInteger monsterTideCount; + private final AtomicInteger monsterKillCount; + private final int monsterSceneLimit; + private final ConcurrentLinkedQueue<Integer> monsterConfigOrders; + + public ScriptMonsterTideService(SceneScriptManager sceneScriptManager, + SceneGroup group, int tideCount, int monsterSceneLimit, Integer[] ordersConfigId){ + this.sceneScriptManager = sceneScriptManager; + this.currentGroup = group; + this.monsterSceneLimit = monsterSceneLimit; + this.monsterTideCount = new AtomicInteger(tideCount); + this.monsterKillCount = new AtomicInteger(0); + this.monsterAlive = new AtomicInteger(0); + this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); + + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterCreatedListener(this::onMonsterCreated); + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterDeadListener(this::onMonsterDead); + // spawn the first turn + for (int i = 0; i < this.monsterSceneLimit; i++) { + this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(group.id, getNextMonster()); + } + } + + public void onMonsterCreated(EntityMonster entityMonster){ + if(this.monsterSceneLimit > 0){ + this.monsterTideCount.decrementAndGet(); + this.monsterAlive.incrementAndGet(); + } + } + + public SceneMonster getNextMonster(){ + var nextId = this.monsterConfigOrders.poll(); + if(currentGroup.monsters.containsKey(nextId)){ + return currentGroup.monsters.get(nextId); + } + // TODO some monster config_id do not exist in groups, so temporarily set it to the first + return currentGroup.monsters.values().stream().findFirst().orElse(null); + } + + public void onMonsterDead(EntityMonster entityMonster){ + if(this.monsterSceneLimit <= 0){ + return; + } + if(this.monsterAlive.decrementAndGet() >= this.monsterSceneLimit) { + // maybe not happen + return; + } + this.monsterKillCount.incrementAndGet(); + if(this.monsterTideCount.get() > 0){ + // add more + this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(this.currentGroup.id, getNextMonster()); + }else if(this.monsterAlive.get() == 0){ + // spawn the last turn of monsters + this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTowerMiddleLevelChangeTeamNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerMiddleLevelChangeTeamNotify.java new file mode 100644 index 000000000..f778c68aa --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTowerMiddleLevelChangeTeamNotify.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TowerMiddleLevelChangeTeamNotifyOuterClass; + +public class PacketTowerMiddleLevelChangeTeamNotify extends BasePacket { + + public PacketTowerMiddleLevelChangeTeamNotify() { + super(PacketOpcodes.TowerMiddleLevelChangeTeamNotify); + + TowerMiddleLevelChangeTeamNotifyOuterClass.TowerMiddleLevelChangeTeamNotify proto = + TowerMiddleLevelChangeTeamNotifyOuterClass.TowerMiddleLevelChangeTeamNotify.newBuilder() + .build(); + + this.setData(proto); + } +} From e23b72f298391e14660b5ddd87f5a22bf83ab6ee Mon Sep 17 00:00:00 2001 From: AnimeGitB <AnimeGitB@bigblueball.in> Date: Fri, 6 May 2022 17:36:13 +0930 Subject: [PATCH 221/434] Add permissionTargeted to applicable commands Change target perm from target.perm to x.perm.others --- .../emu/grasscutter/command/commands/ChangeSceneCommand.java | 2 +- .../java/emu/grasscutter/command/commands/ClearCommand.java | 2 +- src/main/java/emu/grasscutter/command/commands/CoopCommand.java | 2 +- src/main/java/emu/grasscutter/command/commands/DropCommand.java | 2 +- .../emu/grasscutter/command/commands/EnterDungeonCommand.java | 2 +- .../java/emu/grasscutter/command/commands/GiveAllCommand.java | 2 +- .../emu/grasscutter/command/commands/GiveArtifactCommand.java | 2 +- .../java/emu/grasscutter/command/commands/GiveCharCommand.java | 2 +- src/main/java/emu/grasscutter/command/commands/GiveCommand.java | 2 +- .../java/emu/grasscutter/command/commands/GodModeCommand.java | 2 +- src/main/java/emu/grasscutter/command/commands/HealCommand.java | 2 +- .../java/emu/grasscutter/command/commands/KillAllCommand.java | 2 +- .../emu/grasscutter/command/commands/KillCharacterCommand.java | 2 +- .../emu/grasscutter/command/commands/ResetConstCommand.java | 2 +- .../emu/grasscutter/command/commands/ResetShopLimitCommand.java | 2 +- .../emu/grasscutter/command/commands/SendMessageCommand.java | 2 +- .../emu/grasscutter/command/commands/SetFetterLevelCommand.java | 2 +- .../java/emu/grasscutter/command/commands/SetStatsCommand.java | 2 +- .../emu/grasscutter/command/commands/SetWorldLevelCommand.java | 2 +- .../java/emu/grasscutter/command/commands/SpawnCommand.java | 2 +- .../java/emu/grasscutter/command/commands/TalentCommand.java | 2 +- .../emu/grasscutter/command/commands/TeleportAllCommand.java | 2 +- .../java/emu/grasscutter/command/commands/TeleportCommand.java | 2 +- .../java/emu/grasscutter/command/commands/WeatherCommand.java | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index 594eb27c6..51ddb0c5c 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -8,7 +8,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "changescene", usage = "changescene <scene id>", aliases = {"scene"}, permission = "player.changescene", description = "commands.changescene.description") +@Command(label = "changescene", usage = "changescene <scene id>", aliases = {"scene"}, permission = "player.changescene", permissionTargeted = "player.changescene.others", description = "commands.changescene.description") public final class ChangeSceneCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java index 38f78e638..ab0ff0eb1 100644 --- a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java @@ -14,7 +14,7 @@ import static emu.grasscutter.utils.Language.translate; @Command(label = "clear", usage = "clear <all|wp|art|mat>", //Merged /clearartifacts and /clearweapons to /clear <args> [uid] description = "commands.clear.description", - aliases = {"clear"}, permission = "player.clearinv") + aliases = {"clear"}, permission = "player.clearinv", permissionTargeted = "player.clearinv.others") public final class ClearCommand implements CommandHandler { diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index cf7d4fc82..a86472978 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -9,7 +9,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "coop", usage = "coop [host UID]", permission = "server.coop", description = "commands.coop.description") +@Command(label = "coop", usage = "coop [host UID]", permission = "server.coop", permissionTargeted = "server.coop.others", description = "commands.coop.description") public final class CoopCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java index 10c306cad..e2579a7be 100644 --- a/src/main/java/emu/grasscutter/command/commands/DropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -13,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", aliases = {"d", "dropitem"}, permission = "server.drop", description = "commands.drop.description") +@Command(label = "drop", usage = "drop <itemId|itemName> [amount]", aliases = {"d", "dropitem"}, permission = "server.drop", permissionTargeted = "server.drop.others", description = "commands.drop.description") public final class DropCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java index 8534c034f..c4e37a93e 100644 --- a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java @@ -8,7 +8,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", aliases = {"dungeon"}, permission = "player.enterdungeon", description = "commands.enter_dungeon.description") +@Command(label = "enterdungeon", usage = "enterdungeon <dungeon id>", aliases = {"dungeon"}, permission = "player.enterdungeon", permissionTargeted = "player.enterdungeon.others", description = "commands.enter_dungeon.description") public final class EnterDungeonCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index c94d67129..6b9104626 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -15,7 +15,7 @@ import java.util.*; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveall", usage = "giveall [amount]", aliases = {"givea"}, permission = "player.giveall", threading = true, description = "commands.giveAll.description") +@Command(label = "giveall", usage = "giveall [amount]", aliases = {"givea"}, permission = "player.giveall", permissionTargeted = "player.giveall.others", threading = true, description = "commands.giveAll.description") public final class GiveAllCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index cf479ed57..25503dbd3 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -19,7 +19,7 @@ import static java.util.Map.entry; import static emu.grasscutter.utils.Language.translate; -@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart", description = "commands.giveArtifact.description") +@Command(label = "giveart", usage = "giveart <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", aliases = {"gart"}, permission = "player.giveart", permissionTargeted = "player.giveart.others", description = "commands.giveArtifact.description") public final class GiveArtifactCommand implements CommandHandler { private static final Map<String, Map<EquipType, Integer>> mainPropMap = Map.ofEntries( entry("hp", Map.ofEntries(entry(EquipType.EQUIP_BRACER, 14001))), diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 5c6bad0d2..af789f394 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -12,7 +12,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "givechar", usage = "givechar <avatarId> [level]", aliases = {"givec"}, permission = "player.givechar", description = "commands.giveChar.description") +@Command(label = "givechar", usage = "givechar <avatarId> [level]", aliases = {"givec"}, permission = "player.givechar", permissionTargeted = "player.givechar.others", description = "commands.giveChar.description") public final class GiveCharCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index 19a9a8d26..fcac3b9bd 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; import static emu.grasscutter.utils.Language.translate; @Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", aliases = { - "g", "item", "giveitem"}, permission = "player.give", description = "commands.give.description") + "g", "item", "giveitem"}, permission = "player.give", description = "commands.give.description", permissionTargeted = "player.give.others") public final class GiveCommand implements CommandHandler { Pattern lvlRegex = Pattern.compile("l(?:vl?)?(\\d+)"); // Java is a joke of a proglang that doesn't have raw string literals Pattern refineRegex = Pattern.compile("r(\\d+)"); diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java index bf2a00c9f..98a375838 100644 --- a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -8,7 +8,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "godmode", usage = "godmode [on|off|toggle]", permission = "player.godmode", description = "commands.godmode.description") +@Command(label = "godmode", usage = "godmode [on|off|toggle]", permission = "player.godmode", permissionTargeted = "player.godmode.others", description = "commands.godmode.description") public final class GodModeCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java index 440db0a49..78ff14405 100644 --- a/src/main/java/emu/grasscutter/command/commands/HealCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -11,7 +11,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "heal", usage = "heal|h", aliases = {"h"}, permission = "player.heal", description = "commands.heal.description") +@Command(label = "heal", usage = "heal|h", aliases = {"h"}, permission = "player.heal", permissionTargeted = "player.heal.others", description = "commands.heal.description") public final class HealCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java index da9ac7b5e..853395fe6 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -12,7 +12,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killall", usage = "killall [sceneId]", permission = "server.killall", description = "commands.kill.description") +@Command(label = "killall", usage = "killall [sceneId]", permission = "server.killall", permissionTargeted = "server.killall.others", description = "commands.kill.description") public final class KillAllCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 3eda6f7e7..7cff601c4 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -13,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", description = "commands.list.description") +@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others", description = "commands.list.description") public final class KillCharacterCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java index 3a77cee4d..e8229eaf5 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -11,7 +11,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "resetconst", usage = "resetconst [all]", - aliases = {"resetconstellation"}, permission = "player.resetconstellation", description = "commands.resetConst.description") + aliases = {"resetconstellation"}, permission = "player.resetconstellation", permissionTargeted = "player.resetconstellation.others", description = "commands.resetConst.description") public final class ResetConstCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index 7aa84ff6a..99244b813 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -9,7 +9,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", description = "commands.status.description") +@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.status.description") public final class ResetShopLimitCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index 18d6264db..befbf4f00 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -9,7 +9,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "say", usage = "say <message>", - aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage", description = "commands.sendMessage.description") + aliases = {"sendservmsg", "sendservermessage", "sendmessage"}, permission = "server.sendmessage", permissionTargeted = "server.sendmessage.others", description = "commands.sendMessage.description") public final class SendMessageCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index ca5a3cb43..cf356d06b 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -12,7 +12,7 @@ import emu.grasscutter.server.packet.send.PacketAvatarFetterDataNotify; import static emu.grasscutter.utils.Language.translate; @Command(label = "setfetterlevel", usage = "setfetterlevel <level>", - aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel", description = "commands.setFetterLevel.description") + aliases = {"setfetterlvl", "setfriendship"}, permission = "player.setfetterlevel", permissionTargeted = "player.setfetterlevel.others", description = "commands.setFetterLevel.description") public final class SetFetterLevelCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index c7ed78a58..f770ad60b 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -15,7 +15,7 @@ import emu.grasscutter.utils.Language; import static emu.grasscutter.utils.Language.translate; -@Command(label = "setstats", usage = "setstats|stats <stat> <value>", aliases = {"stats"}, permission = "player.setstats", description = "commands.setStats.description") +@Command(label = "setstats", usage = "setstats|stats <stat> <value>", aliases = {"stats"}, permission = "player.setstats", permissionTargeted = "player.setstats.others", description = "commands.setStats.description") public final class SetStatsCommand implements CommandHandler { static class Stat { String name; diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index 41b959336..aa773159e 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -10,7 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "setworldlevel", usage = "setworldlevel <level>", - aliases = {"setworldlvl"}, permission = "player.setworldlevel", description = "commands.setWorldLevel.description") + aliases = {"setworldlvl"}, permission = "player.setworldlevel", permissionTargeted = "player.setworldlevel.others", description = "commands.setWorldLevel.description") public final class SetWorldLevelCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java index 7f0c704c6..897d9ddd2 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -22,7 +22,7 @@ import java.util.Random; import static emu.grasscutter.utils.Language.translate; -@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", permission = "server.spawn", description = "commands.spawn.description") +@Command(label = "spawn", usage = "spawn <entityId> [amount] [level(monster only)]", permission = "server.spawn", permissionTargeted = "server.spawn.others", description = "commands.spawn.description") public final class SpawnCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index 1540a81f8..ac492b580 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -14,7 +14,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "talent", usage = "talent <talentID> <value>", permission = "player.settalent", description = "commands.talent.description") +@Command(label = "talent", usage = "talent <talentID> <value>", permission = "player.settalent", permissionTargeted = "player.settalent.others", description = "commands.talent.description") public final class TalentCommand implements CommandHandler { private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) { int oldLevel = avatar.getSkillLevelMap().get(talentId); diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index 175f69b81..bfa0ac821 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -10,7 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "tpall", usage = "tpall", permission = "player.tpall", description = "commands.teleportAll.description") +@Command(label = "tpall", usage = "tpall", permission = "player.tpall", permissionTargeted = "player.tpall.others", description = "commands.teleportAll.description") public final class TeleportAllCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index 0d15b55af..8a9fb9948 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -10,7 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport", description = "commands.teleport.description") +@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport", permissionTargeted = "player.teleport.others", description = "commands.teleport.description") public final class TeleportCommand implements CommandHandler { private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index dd0002790..0edbd8482 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -11,7 +11,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather", description = "commands.weather.description") +@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather", permissionTargeted = "player.weather.others", description = "commands.weather.description") public final class WeatherCommand implements CommandHandler { @Override From d03f1ca5d1b02664ae388a67c2f870a111143938 Mon Sep 17 00:00:00 2001 From: AnimeGitB <AnimeGitB@bigblueball.in> Date: Sat, 7 May 2022 13:30:55 +0930 Subject: [PATCH 222/434] Account permission wildcards --- .../java/emu/grasscutter/game/Account.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 5b8523ec3..7e8baa291 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -1,6 +1,7 @@ package emu.grasscutter.game; import dev.morphia.annotations.*; +import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Utils; @@ -107,11 +108,41 @@ public class Account { this.permissions.add(permission); return true; } + public static boolean permissionMatchesWildcard(String wildcard, String[] permissionParts) { + String[] wildcardParts = wildcard.split("\\."); + if (permissionParts.length < wildcardParts.length) { // A longer wildcard can never match a shorter permission + return false; + } + for (int i=0; i<wildcardParts.length; i++) { + switch (wildcardParts[i]) { + case "**": // Recursing match + return true; + case "*": // Match only one layer + if (i >= (permissionParts.length-1)) { + return true; + } + break; + default: // This layer isn't a wildcard, it needs to match exactly + if (!wildcardParts[i].equals(permissionParts[i])) { + return false; + } + } + } + // At this point the wildcard will have matched every layer, but if it is shorter then the permission then this is not a match at this point (no **). + return (wildcardParts.length == permissionParts.length); + } + public boolean hasPermission(String permission) { - return this.permissions.contains(permission) || - this.permissions.contains("*") || - (this.permissions.contains("player") || this.permissions.contains("player.*")) && permission.startsWith("player.") || - (this.permissions.contains("server") || this.permissions.contains("server.*")) && permission.startsWith("server."); + if (this.permissions.contains(permission) || this.permissions.contains("*")) { + return true; + } + String[] permissionParts = permission.split("\\."); + for (String p : this.permissions) { + if (permissionMatchesWildcard(p, permissionParts)) { + return true; + } + } + return false; } public boolean removePermission(String permission) { From 993fe3131e382bbabafaea09632c3aa81b85aa53 Mon Sep 17 00:00:00 2001 From: AnimeGitB <AnimeGitB@bigblueball.in> Date: Mon, 9 May 2022 15:33:40 +0930 Subject: [PATCH 223/434] Fix perm field order consistency on GiveCommand --- src/main/java/emu/grasscutter/command/commands/GiveCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index fcac3b9bd..25500f36f 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; import static emu.grasscutter.utils.Language.translate; @Command(label = "give", usage = "give <itemId|itemName> [amount] [level]", aliases = { - "g", "item", "giveitem"}, permission = "player.give", description = "commands.give.description", permissionTargeted = "player.give.others") + "g", "item", "giveitem"}, permission = "player.give", permissionTargeted = "player.give.others", description = "commands.give.description") public final class GiveCommand implements CommandHandler { Pattern lvlRegex = Pattern.compile("l(?:vl?)?(\\d+)"); // Java is a joke of a proglang that doesn't have raw string literals Pattern refineRegex = Pattern.compile("r(\\d+)"); From bf8f4fba522390c6e9b9a2f860ff2fbef091c02d Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Mon, 9 May 2022 17:01:08 +0800 Subject: [PATCH 224/434] Fix #719 --- .../scripts/service/ScriptMonsterSpawnService.java | 4 ++-- .../scripts/service/ScriptMonsterTideService.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java index dda0d4732..0d6baf25d 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java @@ -28,11 +28,11 @@ public class ScriptMonsterSpawnService { onMonsterCreatedListener.add(consumer); } public void addMonsterDeadListener(Consumer<EntityMonster> consumer){ - onMonsterCreatedListener.add(consumer); + onMonsterDeadListener.add(consumer); } public void onMonsterDead(EntityMonster entityMonster){ - onMonsterCreatedListener.stream().forEach(l -> l.accept(entityMonster)); + onMonsterDeadListener.forEach(l -> l.accept(entityMonster)); } public void spawnMonster(int groupId, SceneMonster monster) { if(monster == null){ diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index 117297f15..3cf186188 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -66,9 +66,9 @@ public class ScriptMonsterTideService { if(this.monsterTideCount.get() > 0){ // add more this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(this.currentGroup.id, getNextMonster()); - }else if(this.monsterAlive.get() == 0){ - // spawn the last turn of monsters - this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); } + // spawn the last turn of monsters + // fix the 5-2 + this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); } } From d332e77b762dff8c763efb8a2eec51d725f5492b Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Mon, 9 May 2022 01:43:32 -0700 Subject: [PATCH 225/434] Only handle motion notify for current entity. --- .../StaminaManager/StaminaManager.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 72b91c055..c4b094375 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -243,15 +243,16 @@ public class StaminaManager { cachedEntity = entity; MotionInfo motionInfo = moveInfo.getMotionInfo(); MotionState motionState = motionInfo.getState(); - boolean isReliable = moveInfo.getIsReliable(); - Grasscutter.getLogger().trace("" + motionState + "\t" + (isReliable ? "reliable" : "")); - if (isReliable) { - currentState = motionState; - Vector posVector = motionInfo.getPos(); - Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); - if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { - currentCoordinates = newPos; - } + int notifyEntityId = entity.getId(); + int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId(); + if (notifyEntityId != currentAvatarEntityId) { + return; + } + currentState = motionState; + Vector posVector = motionInfo.getPos(); + Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); + if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { + currentCoordinates = newPos; } startSustainedStaminaHandler(); handleImmediateStamina(session, motionInfo, motionState, entity); From 1430ccb995e4e45b7d486f8a595cefe5193e6cb0 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Mon, 9 May 2022 01:41:00 -0700 Subject: [PATCH 226/434] Exclude macOS junk in case anyone develops on mac. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 35ee889c5..9fa6c9427 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ language/ languages/ gacha-mapping.js data/gacha_mappings.js + +# macOS +.DS_Store From ec7a66861ae3afc50872ef5ca1cdab10fc16ddca Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Mon, 9 May 2022 17:01:08 +0800 Subject: [PATCH 227/434] Fix #719 --- .../scripts/service/ScriptMonsterSpawnService.java | 4 ++-- .../scripts/service/ScriptMonsterTideService.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java index dda0d4732..0d6baf25d 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java @@ -28,11 +28,11 @@ public class ScriptMonsterSpawnService { onMonsterCreatedListener.add(consumer); } public void addMonsterDeadListener(Consumer<EntityMonster> consumer){ - onMonsterCreatedListener.add(consumer); + onMonsterDeadListener.add(consumer); } public void onMonsterDead(EntityMonster entityMonster){ - onMonsterCreatedListener.stream().forEach(l -> l.accept(entityMonster)); + onMonsterDeadListener.forEach(l -> l.accept(entityMonster)); } public void spawnMonster(int groupId, SceneMonster monster) { if(monster == null){ diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index 117297f15..3cf186188 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -66,9 +66,9 @@ public class ScriptMonsterTideService { if(this.monsterTideCount.get() > 0){ // add more this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(this.currentGroup.id, getNextMonster()); - }else if(this.monsterAlive.get() == 0){ - // spawn the last turn of monsters - this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); } + // spawn the last turn of monsters + // fix the 5-2 + this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); } } From 56635f2ecccd757d6d2e9db8693c5417a8026f3f Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Mon, 9 May 2022 03:42:03 -0700 Subject: [PATCH 228/434] Move "if Grasscutter.getConfig().OpenStamina" to the correct place. --- .../StaminaManager/StaminaManager.java | 97 ++++++++++--------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index c4b094375..1156993b1 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -190,14 +190,17 @@ public class StaminaManager { // Returns new stamina and sends PlayerPropNotify public int setStamina(GameSession session, String reason, int newStamina) { - // set stamina - player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); - session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - // notify updated - for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { - listener.getValue().onAfterUpdateStamina(reason, newStamina); + if (Grasscutter.getConfig().OpenStamina) { + // set stamina + player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); + // notify updated + for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { + listener.getValue().onAfterUpdateStamina(reason, newStamina); + } + return newStamina; } - return newStamina; + return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); } // Kills avatar, removes entity and sends notification. @@ -288,50 +291,48 @@ public class StaminaManager { private class SustainedStaminaHandler extends TimerTask { public void run() { - if (Grasscutter.getConfig().OpenStamina) { - boolean moving = isPlayerMoving(); - int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); - int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); - if (moving || (currentStamina < maxStamina)) { - Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " + - (currentStamina >= maxStamina) + ", recalculate stamina"); + boolean moving = isPlayerMoving(); + int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); + if (moving || (currentStamina < maxStamina)) { + Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " + + (currentStamina >= maxStamina) + ", recalculate stamina"); - Consumption consumption = new Consumption(ConsumptionType.None); - if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - consumption = getClimbSustainedConsumption(); - } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { - consumption = getSwimSustainedConsumptions(); - } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - consumption = getRunWalkDashSustainedConsumption(); - } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - consumption = getFlySustainedConsumption(); - } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - consumption = getStandSustainedConsumption(); - } + Consumption consumption = new Consumption(ConsumptionType.None); + if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { + consumption = getClimbSustainedConsumption(); + } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { + consumption = getSwimSustainedConsumptions(); + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + consumption = getRunWalkDashSustainedConsumption(); + } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { + consumption = getFlySustainedConsumption(); + } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { + consumption = getStandSustainedConsumption(); + } - /* - TODO: Reductions that apply to all motion types: - Elemental Resonance - Wind: -15% - Skills - Diona E: -10% while shield lasts - Barbara E: -12% while lasts - */ - if (cachedSession != null) { - if (consumption.amount < 0) { - staminaRecoverDelay = 0; - } - if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { - // For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. - if (staminaRecoverDelay < 10) { - // For others recover after 2 seconds (10 ticks) - as official server does. - staminaRecoverDelay++; - consumption.amount = 0; - Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); - } - } - updateStaminaRelative(cachedSession, consumption); + /* + TODO: Reductions that apply to all motion types: + Elemental Resonance + Wind: -15% + Skills + Diona E: -10% while shield lasts + Barbara E: -12% while lasts + */ + if (cachedSession != null) { + if (consumption.amount < 0) { + staminaRecoverDelay = 0; } + if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { + // For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. + if (staminaRecoverDelay < 10) { + // For others recover after 2 seconds (10 ticks) - as official server does. + staminaRecoverDelay++; + consumption.amount = 0; + Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); + } + } + updateStaminaRelative(cachedSession, consumption); } } previousState = currentState; From c2d41ca0cfcad5f6555bd397aaa98a8a47ca3036 Mon Sep 17 00:00:00 2001 From: AnimeGitB <AnimeGitB@bigblueball.in> Date: Mon, 9 May 2022 20:21:40 +0930 Subject: [PATCH 229/434] Fix /talent saving to level instead of id --- .../java/emu/grasscutter/command/commands/TalentCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index ac492b580..40ac11b50 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -24,7 +24,7 @@ public final class TalentCommand implements CommandHandler { } // Upgrade skill - avatar.getSkillLevelMap().put(talentLevel, talentLevel); + avatar.getSkillLevelMap().put(talentId, talentLevel); avatar.save(); // Packet From e6cf27056fb3f6ed894e25173812184eba3ea843 Mon Sep 17 00:00:00 2001 From: Mateoust <46558043+Mateoust@users.noreply.github.com> Date: Mon, 9 May 2022 19:03:34 +0800 Subject: [PATCH 230/434] Updating language files fixed the issue 648 677 fix issue 648 677 --- src/main/resources/languages/en-US.json | 7 +++++++ src/main/resources/languages/pl-PL.json | 9 ++++++++- src/main/resources/languages/zh-CN.json | 11 +++++++++-- src/main/resources/languages/zh-TW.json | 11 +++++++++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 4ec17a214..9e7271ea3 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -334,6 +334,13 @@ }, "restart": { "description": "Restarts the current session" + }, + "unlocktower": { + "success": "unlock done", + "description": "Unlock all levels of tower" + }, + "resetshop": { + "description": "reset shop" } } } diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json index e9ff74e25..8f76d8951 100644 --- a/src/main/resources/languages/pl-PL.json +++ b/src/main/resources/languages/pl-PL.json @@ -184,7 +184,7 @@ "account_error": "Konto nie może zostać znalezione." }, "position": { - "success": "Koordynaty: %.3f, %.3f, %.3f\nID sceny: %d" + "success": "Koordynaty: %s, %s, %s\nID sceny: %s" }, "reload": { "reload_start": "Ponowne ładowanie konfiguracji.", @@ -293,6 +293,13 @@ "usage": "Użycie: ", "aliases": "Aliasy: ", "available_commands": "Dostępne komendy: " + }, + "unlocktower": { + "success": "odblokować gotowe", + "description": "Odblokuj głęboką spiralę" + }, + "resetshop": { + "description": "zresetuj sklep" } } } \ No newline at end of file diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 89d1d6a90..f9e750438 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -201,7 +201,7 @@ "description": "给予或移除指定玩家的权限。" }, "position": { - "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d", + "success": "坐标:%s, %s, %s\n场景ID:%s", "description": "获取所在位置。" }, "reload": { @@ -249,7 +249,7 @@ "setFetterLevel": { "usage": "用法:setfetterlevel <level>", "range_error": "好感度等级必须在 0 到 10 之间。", - "fetter_set_level": "好感度已设置为 %s 级", + "success": "好感度已设置为 %s 级", "level_error": "无效的好感度等级。", "description": "设置当前角色的好感度等级。" }, @@ -330,6 +330,13 @@ }, "restart": { "description": "重新启动服务器。" + }, + "unlocktower": { + "success": "解锁完成。", + "description": "解锁深境螺旋的所有层" + }, + "resetshop": { + "description": "重置商店时间" } } } diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 7a7f5c100..9c3c99686 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -184,7 +184,7 @@ "account_error": "The account cannot be found." }, "position": { - "success": "坐標:%.3f, %.3f, %.3f\n場景ID:%d" + "success": "坐標:%s, %s, %s\n場景ID:%s" }, "reload": { "reload_start": "正在重新加載設定檔。", @@ -226,7 +226,7 @@ "setFetterLevel": { "usage": "用法:setfetterlevel <level>", "range_error": "好感度必須在 0 到 10 之間。", - "fetter_set_level": "好感等級已設定為 %s", + "success": "好感等級已設定為 %s", "level_error": "無效的好感度。" }, "setStats": { @@ -293,6 +293,13 @@ "usage": "用法:", "aliases": "別名:", "available_commands": "可用指令:" + }, + "unlocktower": { + "success": "解鎖完成。", + "description": "解鎖所有級別的深境螺旋" + }, + "resetshop": { + "description": "重置商店時間" } } } From a63d87b0bc26e27a8800e1cc490154fdf34cd255 Mon Sep 17 00:00:00 2001 From: Mateoust <46558043+Mateoust@users.noreply.github.com> Date: Mon, 9 May 2022 19:05:03 +0800 Subject: [PATCH 231/434] Update translation keys to fix issues 648 677 fix issues 648 677 --- src/main/java/emu/grasscutter/command/CommandMap.java | 6 +++--- .../grasscutter/command/commands/ChangeSceneCommand.java | 5 +++-- .../grasscutter/command/commands/ResetShopLimitCommand.java | 2 +- .../emu/grasscutter/command/commands/SetStatsCommand.java | 2 +- .../java/emu/grasscutter/command/commands/SpawnCommand.java | 6 +++--- .../grasscutter/command/commands/UnlockTowerCommand.java | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java index a183c6ac3..2ff105b31 100644 --- a/src/main/java/emu/grasscutter/command/CommandMap.java +++ b/src/main/java/emu/grasscutter/command/CommandMap.java @@ -150,7 +150,7 @@ public final class CommandMap { int uid = Integer.parseInt(targetUidStr); targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid); if (targetPlayer == null) { - CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); + CommandHandler.sendMessage(player, translate("commands.execution.player_exist_offline_error")); } else { targetPlayerIds.put(playerId, uid); CommandHandler.sendMessage(player, translate("commands.execution.set_target", targetUidStr)); @@ -178,7 +178,7 @@ public final class CommandMap { int uid = Integer.parseInt(arg); targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid); if (targetPlayer == null) { - CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); + CommandHandler.sendMessage(player, translate("commands.execution.player_exist_offline_error")); return; } break; @@ -194,7 +194,7 @@ public final class CommandMap { if (targetPlayerIds.containsKey(playerId)) { targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId)); // We check every time in case the target goes offline after being targeted if (targetPlayer == null) { - CommandHandler.sendMessage(player, translate("commands.generic.execution.player_exist_offline_error")); + CommandHandler.sendMessage(player, translate("commands.execution.player_exist_offline_error")); return; } } else { diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index 51ddb0c5c..59706dd96 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -31,11 +31,12 @@ public final class ChangeSceneCommand implements CommandHandler { } boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, targetPlayer.getPos()); - CommandHandler.sendMessage(sender, translate("commands.changescene.result", Integer.toString(sceneId))); - if (!result) { CommandHandler.sendMessage(sender, translate("commands.changescene.exists_error")); + return; } + + CommandHandler.sendMessage(sender, translate("commands.changescene.success", Integer.toString(sceneId))); } catch (Exception e) { CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); } diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index 99244b813..bf5e2a4a6 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -9,7 +9,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.status.description") +@Command(label = "resetshop", usage = "resetshop", permission = "server.resetshop", permissionTargeted = "server.resetshop.others", description = "commands.resetshop.description") public final class ResetShopLimitCommand implements CommandHandler { @Override diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index f770ad60b..a0572d2c2 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -175,7 +175,7 @@ public final class SetStatsCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.ingame"); + String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.usage_ingame"); String usage = syntax + translate("commands.setStats.help_message"); String statStr; String valueStr; diff --git a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java index 897d9ddd2..e3193c638 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -46,13 +46,13 @@ public final class SpawnCommand implements CommandHandler { try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.error.amount")); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); } // Fallthrough case 1: try { id = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.error.entityId")); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.entityId")); } break; default: @@ -64,7 +64,7 @@ public final class SpawnCommand implements CommandHandler { GadgetData gadgetData = GameData.getGadgetDataMap().get(id); ItemData itemData = GameData.getItemDataMap().get(id); if (monsterData == null && gadgetData == null && itemData == null) { - CommandHandler.sendMessage(sender, translate("commands.generic.error.entityId")); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.entityId")); return; } Scene scene = targetPlayer.getScene(); diff --git a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java index e0fce695c..bd7b8bc1f 100644 --- a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java @@ -10,7 +10,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; @Command(label = "unlocktower", usage = "unlocktower", aliases = {"ut"}, - description = "Unlock all levels of tower", permission = "player.tower") + description = "commands.unlocktower.description", permission = "player.tower") public class UnlockTowerCommand implements CommandHandler { @Override @@ -21,7 +21,7 @@ public class UnlockTowerCommand implements CommandHandler { unlockFloor(sender, sender.getServer().getTowerScheduleManager() .getScheduleFloors()); - CommandHandler.sendMessage(sender, translate("commands.tower.unlock_done")); + CommandHandler.sendMessage(sender, translate("commands.unlocktower.success")); } public void unlockFloor(Player player, List<Integer> floors){ From 5a62a69c734d31c0f7b4047450fc67ee4be3d867 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Tue, 10 May 2022 00:05:01 +0800 Subject: [PATCH 232/434] fix the Monster spawn between stage challenges --- data/TowerSchedule.json | 2 +- .../game/dungeons/DungeonChallenge.java | 28 ++++++++-- .../game/entity/EntityMonster.java | 12 ++-- .../grasscutter/game/tower/TowerManager.java | 2 +- .../scripts/SceneScriptManager.java | 11 +++- .../emu/grasscutter/scripts/ScriptLib.java | 28 ++++++++-- .../scripts/data/SceneTrigger.java | 11 ++++ .../listener/ScriptMonsterListener.java | 8 +++ .../service/ScriptMonsterSpawnService.java | 25 +++++---- .../service/ScriptMonsterTideService.java | 56 ++++++++++++------- 10 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 src/main/java/emu/grasscutter/scripts/listener/ScriptMonsterListener.java diff --git a/data/TowerSchedule.json b/data/TowerSchedule.json index b93100645..e10416a20 100644 --- a/data/TowerSchedule.json +++ b/data/TowerSchedule.json @@ -1,5 +1,5 @@ { - "scheduleId" : 1, + "scheduleId" : 45, "scheduleStartTime" : "2022-05-01T00:00:00+08:00", "nextScheduleChangeTime" : "2022-05-30T00:00:00+08:00" } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java index 2e07f0058..a13f1a611 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonChallenge.java @@ -28,14 +28,20 @@ public class DungeonChallenge { private int challengeId; private boolean success; private boolean progress; - + /** + * has more challenge + */ + private boolean stage; private int score; private int objective = 0; private IntSet rewardedPlayers; - public DungeonChallenge(Scene scene, SceneGroup group) { + public DungeonChallenge(Scene scene, SceneGroup group, int challengeId, int challengeIndex, int objective) { this.scene = scene; this.group = group; + this.challengeId = challengeId; + this.challengeIndex = challengeIndex; + this.objective = objective; this.setRewardedPlayers(new IntOpenHashSet()); } @@ -86,7 +92,15 @@ public class DungeonChallenge { public int getScore() { return score; } - + + public boolean isStage() { + return stage; + } + + public void setStage(boolean stage) { + this.stage = stage; + } + public int getTimeLimit() { return 600; } @@ -112,7 +126,7 @@ public class DungeonChallenge { if (this.isSuccess()) { // Call success script event this.getScene().getScriptManager().callEvent(EventType.EVENT_CHALLENGE_SUCCESS, null); - + // Settle settle(); } else { @@ -122,8 +136,10 @@ public class DungeonChallenge { private void settle() { getScene().getDungeonSettleObservers().forEach(o -> o.onDungeonSettle(getScene())); - - getScene().getScriptManager().callEvent(EventType.EVENT_DUNGEON_SETTLE, new ScriptArgs(this.isSuccess() ? 1 : 0)); + + if(!stage){ + getScene().getScriptManager().callEvent(EventType.EVENT_DUNGEON_SETTLE, new ScriptArgs(this.isSuccess() ? 1 : 0)); + } } public void onMonsterDie(EntityMonster entity) { diff --git a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java index f8a96a808..491aaa3f4 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java @@ -116,14 +116,18 @@ public class EntityMonster extends GameEntity { if (this.getSpawnEntry() != null) { this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); } + // first set the challenge data + if (getScene().getChallenge() != null && getScene().getChallenge().getGroup().id == this.getGroupId()) { + getScene().getChallenge().onMonsterDie(this); + } if (getScene().getScriptManager().isInit() && this.getGroupId() > 0) { if(getScene().getScriptManager().getScriptMonsterSpawnService() != null){ getScene().getScriptManager().getScriptMonsterSpawnService().onMonsterDead(this); } - getScene().getScriptManager().callEvent(EventType.EVENT_ANY_MONSTER_DIE, null); - } - if (getScene().getChallenge() != null && getScene().getChallenge().getGroup().id == this.getGroupId()) { - getScene().getChallenge().onMonsterDie(this); + // prevent spawn monster after success + if(getScene().getChallenge() != null && getScene().getChallenge().inProgress()){ + getScene().getScriptManager().callEvent(EventType.EVENT_ANY_MONSTER_DIE, null); + } } } diff --git a/src/main/java/emu/grasscutter/game/tower/TowerManager.java b/src/main/java/emu/grasscutter/game/tower/TowerManager.java index cac848ea6..790efc7f6 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerManager.java @@ -122,7 +122,7 @@ public class TowerManager { if(!hasNextLevel()){ // set up the next floor - recordMap.put(getNextFloorId(), new TowerLevelRecord(getNextFloorId())); + recordMap.putIfAbsent(getNextFloorId(), new TowerLevelRecord(getNextFloorId())); player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(getNextFloorId(), 1)); }else{ player.getSession().send(new PacketTowerCurLevelRecordChangeNotify(currentFloorId, getCurrentLevel())); diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index bc0e896f5..5b86aa298 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -373,6 +373,12 @@ public class SceneScriptManager { new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId); } + public void unloadCurrentMonsterTide(){ + if(this.getScriptMonsterTideService() == null){ + return; + } + this.getScriptMonsterTideService().unload(); + } public void spawnMonstersByConfigId(int configId, int delayTime) { // TODO delay this.scriptMonsterSpawnService.spawnMonster(this.currentGroup.id, this.currentGroup.monsters.get(configId)); @@ -395,12 +401,15 @@ public class SceneScriptManager { if (params != null) { args = CoerceJavaToLua.coerce(params); } - + + ScriptLib.logger.trace("Call Condition Trigger {}", trigger); ret = safetyCall(trigger.condition, condition, args); } if (ret.isboolean() && ret.checkboolean()) { + ScriptLib.logger.trace("Call Action Trigger {}", trigger); LuaValue action = (LuaValue) this.getBindings().get(trigger.action); + // TODO impl the param of SetGroupVariableValueByGroup var arg = new ScriptArgs(); arg.param2 = 100; var args = CoerceJavaToLua.coerce(arg); diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 0d686dd5a..b7fb5939f 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -147,6 +147,12 @@ public class ScriptLib { return 1; } + // avoid spawn wrong monster + if(getSceneScriptManager().getScene().getChallenge() != null) + if(!getSceneScriptManager().getScene().getChallenge().inProgress() || + getSceneScriptManager().getScene().getChallenge().getGroup().id != groupId){ + return 0; + } this.getSceneScriptManager().spawnMonstersInGroup(group, suite); return 0; @@ -175,13 +181,13 @@ public class ScriptLib { return 0; } - DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), group); - challenge.setChallengeId(challengeId); - challenge.setChallengeIndex(challengeIndex); - challenge.setObjective(objective); - + DungeonChallenge challenge = new DungeonChallenge(getSceneScriptManager().getScene(), + group, challengeId, challengeIndex, objective); + // set if tower first stage (6-1) + challenge.setStage(getSceneScriptManager().getVariables().getOrDefault("stage", -1) == 0); + getSceneScriptManager().getScene().setChallenge(challenge); - + challenge.start(); return 0; } @@ -336,9 +342,19 @@ public class ScriptLib { logger.debug("[LUA] Call TowerMirrorTeamSetUp with {},{}", team,var1); + getSceneScriptManager().unloadCurrentMonsterTide(); getSceneScriptManager().getScene().getPlayers().get(0).getTowerManager().mirrorTeamSetUp(team-1); return 0; } + public int CreateGadget(LuaTable table){ + logger.debug("[LUA] Call CreateGadget with {}", + printTable(table)); + var configId = table.get("config_id").toint(); + + //TODO + + return 0; + } } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java index a627f67c4..301fdb8e0 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java @@ -21,4 +21,15 @@ public class SceneTrigger { return name.hashCode(); } + @Override + public String toString() { + return "SceneTrigger{" + + "name='" + name + '\'' + + ", config_id=" + config_id + + ", event=" + event + + ", source='" + source + '\'' + + ", condition='" + condition + '\'' + + ", action='" + action + '\'' + + '}'; + } } diff --git a/src/main/java/emu/grasscutter/scripts/listener/ScriptMonsterListener.java b/src/main/java/emu/grasscutter/scripts/listener/ScriptMonsterListener.java new file mode 100644 index 000000000..b3b99fd61 --- /dev/null +++ b/src/main/java/emu/grasscutter/scripts/listener/ScriptMonsterListener.java @@ -0,0 +1,8 @@ +package emu.grasscutter.scripts.listener; + +import emu.grasscutter.game.entity.EntityMonster; + +public interface ScriptMonsterListener { + + void onNotify(EntityMonster sceneMonster); +} diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java index 0d6baf25d..fdc4941f2 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterSpawnService.java @@ -8,31 +8,36 @@ import emu.grasscutter.scripts.SceneScriptManager; import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.scripts.listener.ScriptMonsterListener; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; public class ScriptMonsterSpawnService { private final SceneScriptManager sceneScriptManager; - private final List<Consumer<EntityMonster>> onMonsterCreatedListener = new ArrayList<>(); + private final List<ScriptMonsterListener> onMonsterCreatedListener = new ArrayList<>(); - private final List<Consumer<EntityMonster>> onMonsterDeadListener = new ArrayList<>(); + private final List<ScriptMonsterListener> onMonsterDeadListener = new ArrayList<>(); public ScriptMonsterSpawnService(SceneScriptManager sceneScriptManager){ this.sceneScriptManager = sceneScriptManager; } - public void addMonsterCreatedListener(Consumer<EntityMonster> consumer){ - onMonsterCreatedListener.add(consumer); + public void addMonsterCreatedListener(ScriptMonsterListener scriptMonsterListener){ + onMonsterCreatedListener.add(scriptMonsterListener); } - public void addMonsterDeadListener(Consumer<EntityMonster> consumer){ - onMonsterDeadListener.add(consumer); + public void addMonsterDeadListener(ScriptMonsterListener scriptMonsterListener){ + onMonsterDeadListener.add(scriptMonsterListener); + } + public void removeMonsterCreatedListener(ScriptMonsterListener scriptMonsterListener){ + onMonsterCreatedListener.remove(scriptMonsterListener); + } + public void removeMonsterDeadListener(ScriptMonsterListener scriptMonsterListener){ + onMonsterDeadListener.remove(scriptMonsterListener); } - public void onMonsterDead(EntityMonster entityMonster){ - onMonsterDeadListener.forEach(l -> l.accept(entityMonster)); + onMonsterDeadListener.forEach(l -> l.onNotify(entityMonster)); } public void spawnMonster(int groupId, SceneMonster monster) { if(monster == null){ @@ -64,7 +69,7 @@ public class ScriptMonsterSpawnService { entity.setGroupId(groupId); entity.setConfigId(monster.config_id); - onMonsterCreatedListener.forEach(action -> action.accept(entity)); + onMonsterCreatedListener.forEach(action -> action.onNotify(entity)); sceneScriptManager.getScene().addEntity(entity); diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index 3cf186188..cf23aaca9 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -6,6 +6,7 @@ import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.scripts.listener.ScriptMonsterListener; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; @@ -19,6 +20,8 @@ public class ScriptMonsterTideService { private final AtomicInteger monsterKillCount; private final int monsterSceneLimit; private final ConcurrentLinkedQueue<Integer> monsterConfigOrders; + private final OnMonsterCreated onMonsterCreated= new OnMonsterCreated(); + private final OnMonsterDead onMonsterDead= new OnMonsterDead(); public ScriptMonsterTideService(SceneScriptManager sceneScriptManager, SceneGroup group, int tideCount, int monsterSceneLimit, Integer[] ordersConfigId){ @@ -30,18 +33,21 @@ public class ScriptMonsterTideService { this.monsterAlive = new AtomicInteger(0); this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); - this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterCreatedListener(this::onMonsterCreated); - this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterDeadListener(this::onMonsterDead); + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterCreatedListener(onMonsterCreated); + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterDeadListener(onMonsterDead); // spawn the first turn for (int i = 0; i < this.monsterSceneLimit; i++) { this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(group.id, getNextMonster()); } } - public void onMonsterCreated(EntityMonster entityMonster){ - if(this.monsterSceneLimit > 0){ - this.monsterTideCount.decrementAndGet(); - this.monsterAlive.incrementAndGet(); + public class OnMonsterCreated implements ScriptMonsterListener{ + @Override + public void onNotify(EntityMonster sceneMonster) { + if(monsterSceneLimit > 0){ + monsterAlive.incrementAndGet(); + monsterTideCount.decrementAndGet(); + } } } @@ -54,21 +60,29 @@ public class ScriptMonsterTideService { return currentGroup.monsters.values().stream().findFirst().orElse(null); } - public void onMonsterDead(EntityMonster entityMonster){ - if(this.monsterSceneLimit <= 0){ - return; + public class OnMonsterDead implements ScriptMonsterListener{ + @Override + public void onNotify(EntityMonster sceneMonster) { + if(monsterSceneLimit <= 0){ + return; + } + if(monsterAlive.decrementAndGet() >= monsterSceneLimit) { + // maybe not happen + return; + } + monsterKillCount.incrementAndGet(); + if(monsterTideCount.get() > 0){ + // add more + sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(currentGroup.id, getNextMonster()); + } + // spawn the last turn of monsters + // fix the 5-2 + sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(monsterKillCount.get())); } - if(this.monsterAlive.decrementAndGet() >= this.monsterSceneLimit) { - // maybe not happen - return; - } - this.monsterKillCount.incrementAndGet(); - if(this.monsterTideCount.get() > 0){ - // add more - this.sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(this.currentGroup.id, getNextMonster()); - } - // spawn the last turn of monsters - // fix the 5-2 - this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); + } + + public void unload(){ + this.sceneScriptManager.getScriptMonsterSpawnService().removeMonsterCreatedListener(onMonsterCreated); + this.sceneScriptManager.getScriptMonsterSpawnService().removeMonsterDeadListener(onMonsterDead); } } From 0764b6c2fac10ec9260be7869c177802eda178cf Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Tue, 10 May 2022 00:14:50 +0800 Subject: [PATCH 233/434] little fix --- .../scripts/service/ScriptMonsterTideService.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index 878438190..57d4735ba 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -60,18 +60,18 @@ public class ScriptMonsterTideService { return currentGroup.monsters.values().stream().findFirst().orElse(null); } - public class OnMonsterDead implements ScriptMonsterListener{ + public class OnMonsterDead implements ScriptMonsterListener { @Override public void onNotify(EntityMonster sceneMonster) { - if(monsterSceneLimit <= 0){ + if (monsterSceneLimit <= 0) { return; } - if(monsterAlive.decrementAndGet() >= monsterSceneLimit) { + if (monsterAlive.decrementAndGet() >= monsterSceneLimit) { // maybe not happen return; } monsterKillCount.incrementAndGet(); - if(monsterTideCount.get() > 0){ + if (monsterTideCount.get() > 0) { // add more sceneScriptManager.getScriptMonsterSpawnService().spawnMonster(currentGroup.id, getNextMonster()); } @@ -79,9 +79,7 @@ public class ScriptMonsterTideService { // fix the 5-2 sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(monsterKillCount.get())); } - // spawn the last turn of monsters - // fix the 5-2 - this.sceneScriptManager.callEvent(EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(this.monsterKillCount.get())); + } public void unload(){ From 32232ff6f91886cebcd4c220eec6a529b036ef51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E5=82=91?= <jie65535@qq.com> Date: Mon, 9 May 2022 20:44:46 +0800 Subject: [PATCH 234/434] Fixed MissingFormatArgumentException (#727) Format string is `"given_level": "Given %s with level %s %s times to %s"` --- src/main/java/emu/grasscutter/command/commands/GiveCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index 25500f36f..a60991bad 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -123,7 +123,7 @@ public final class GiveCommand implements CommandHandler { } else if (itemData.getItemType() == ItemType.ITEM_WEAPON) { CommandHandler.sendMessage(sender, translate("commands.give.given_with_level_and_refinement", Integer.toString(item), Integer.toString(lvl), Integer.toString(refinement), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); } else { - CommandHandler.sendMessage(sender, translate("commands.give.given_level", Integer.toString(item), Integer.toString(lvl), Integer.toString(amount))); + CommandHandler.sendMessage(sender, translate("commands.give.given_level", Integer.toString(item), Integer.toString(lvl), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); } } From b7ed76c79b84617df38e2850a166416c5613e915 Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Mon, 9 May 2022 10:03:51 -0700 Subject: [PATCH 235/434] Change ccount delete to delete all referenced items in the database. --- .../command/commands/AccountCommand.java | 21 ++++++++++++-- .../grasscutter/database/DatabaseHelper.java | 29 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 4b287afa7..a16cc6480 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -1,8 +1,10 @@ package emu.grasscutter.command.commands; +import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; import emu.grasscutter.game.player.Player; import java.util.List; @@ -54,11 +56,24 @@ public final class AccountCommand implements CommandHandler { } return; case "delete": - if (DatabaseHelper.deleteAccount(username)) { - CommandHandler.sendMessage(null, translate("commands.account.delete")); - } else { + // Get the account we want to delete. + Account toDelete = DatabaseHelper.getAccountByName(username); + + if (toDelete == null) { CommandHandler.sendMessage(null, translate("commands.account.no_account")); + return; } + + // Get the player for the account. + // If that player is currently online, we kick them before proceeding with the deletion. + Player player = Grasscutter.getGameServer().getPlayerByUid(toDelete.getPlayerUid()); + if (player != null) { + player.getSession().close(); + } + + // Finally, we do the actual deletion. + DatabaseHelper.deleteAccount(toDelete); + CommandHandler.sendMessage(null, translate("commands.account.delete")); } } } diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index f63798988..14a3e1c72 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -3,6 +3,8 @@ package emu.grasscutter.database; import java.util.List; import com.mongodb.client.result.DeleteResult; + +import dev.morphia.experimental.MorphiaSession; import dev.morphia.query.FindOptions; import dev.morphia.query.Sort; import dev.morphia.query.experimental.filters.Filters; @@ -95,8 +97,31 @@ public final class DatabaseHelper { return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first(); } - public static boolean deleteAccount(String username) { - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).delete().getDeletedCount() > 0; + //public static boolean deleteAccount(String username) { + // return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).delete().getDeletedCount() > 0; + //} + public static void deleteAccount(Account target) { + // To delete an account, we need to also delete all the other documents in the database that reference the account. + // This should optimally be wrapped inside a transaction, to make sure an error thrown mid-way does not leave the + // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... + + // Delete mails, gacha records, items and avatars. + DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", target.getPlayerUid())).delete(); + DatabaseManager.getDatastore().find(GachaRecord.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); + DatabaseManager.getDatastore().find(GameItem.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); + DatabaseManager.getDatastore().find(Avatar.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); + + // Delete friendships. + // Here, we need to make sure to not only delete the deleted account's friendships, + // but also all friendship entries for that account's friends. + DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); + DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("friendId", target.getPlayerUid())).delete(); + + // Delete the player. + DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete(); + + // Finally, delete the account itself. + DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("id", target.getId())).delete(); } public static List<Player> getAllPlayers() { From 5bf22ce8a29b6f74cc0fc029bc9095c0223dced3 Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Mon, 9 May 2022 10:09:19 -0700 Subject: [PATCH 236/434] Remove commented method and unused import. --- src/main/java/emu/grasscutter/database/DatabaseHelper.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 14a3e1c72..bfcd006df 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -4,7 +4,6 @@ import java.util.List; import com.mongodb.client.result.DeleteResult; -import dev.morphia.experimental.MorphiaSession; import dev.morphia.query.FindOptions; import dev.morphia.query.Sort; import dev.morphia.query.experimental.filters.Filters; @@ -97,14 +96,11 @@ public final class DatabaseHelper { return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first(); } - //public static boolean deleteAccount(String username) { - // return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).delete().getDeletedCount() > 0; - //} public static void deleteAccount(Account target) { // To delete an account, we need to also delete all the other documents in the database that reference the account. // This should optimally be wrapped inside a transaction, to make sure an error thrown mid-way does not leave the // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... - + // Delete mails, gacha records, items and avatars. DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", target.getPlayerUid())).delete(); DatabaseManager.getDatastore().find(GachaRecord.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); From e3daf684a5c013dd98c78a36b1d2492093a5637e Mon Sep 17 00:00:00 2001 From: Kinesis <CCasusensa@users.noreply.github.com> Date: Mon, 9 May 2022 23:10:35 +0800 Subject: [PATCH 237/434] fixed error KillCharacter description in the help command --- .../emu/grasscutter/command/commands/KillCharacterCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 7cff601c4..7435c6d5f 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -13,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others", description = "commands.list.description") +@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others", description = "commands.killcharacter.description") public final class KillCharacterCommand implements CommandHandler { @Override From f4f2cf6a96ff5ae40f03b0c7290f5127a7f11c63 Mon Sep 17 00:00:00 2001 From: hatsune-miku <eggtart@mikutart.com> Date: Mon, 9 May 2022 17:51:46 -0230 Subject: [PATCH 238/434] Disable falling damage for godmode --- .../server/packet/recv/HandlerCombatInvocationsNotify.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 50fca5101..1ce089c26 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -102,6 +102,10 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { if (cachedLandingSpeed < -28) { damageFactor = 1f; } + // Disable falling damage for players in god mode. + if (session.getPlayer() != null && session.getPlayer().inGodmode()) { + damageFactor = 0; + } float damage = maxHP * damageFactor; float newHP = currentHP - damage; if (newHP < 0) { From e4df2813f1e50fe2220a1d979f4a92363a328326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9C=9F=E5=BF=83?= <ceo@iqianye.cn> Date: Tue, 10 May 2022 13:13:42 +0800 Subject: [PATCH 239/434] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dd8fa9fa0..4434ed28e 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' -version = '1.1.0' +version = '1.1.1-dev' sourceCompatibility = 17 targetCompatibility = 17 From 06be99fa155d0cfae45ba0a89abc42ae19518487 Mon Sep 17 00:00:00 2001 From: FpguDhk <35809384+341101@users.noreply.github.com> Date: Tue, 10 May 2022 14:15:17 +0800 Subject: [PATCH 240/434] Fix the decision statement of adding map marker. (#763) * Fix the Chinese messy code problem. * Fix the decision statement of adding map marker. --- .../game/managers/MapMarkManager/MapMarksManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/managers/MapMarkManager/MapMarksManager.java b/src/main/java/emu/grasscutter/game/managers/MapMarkManager/MapMarksManager.java index 5249f59d9..d014ce204 100644 --- a/src/main/java/emu/grasscutter/game/managers/MapMarkManager/MapMarksManager.java +++ b/src/main/java/emu/grasscutter/game/managers/MapMarkManager/MapMarksManager.java @@ -46,7 +46,7 @@ public class MapMarksManager { public boolean addMapMark(MapMark mapMark) { if (mapMarks.size() < mapMarkMaxCount) { - if (!mapMarks.containsKey(mapMark.getPosition())) { + if (!mapMarks.containsKey(getMapMarkKey(mapMark.getPosition()))) { mapMarks.put(getMapMarkKey(mapMark.getPosition()), mapMark); return true; } From cba16f286845ad7613d7abca334432416b5ca450 Mon Sep 17 00:00:00 2001 From: Secretboy-SMR <secretboy.smr@icloud.com> Date: Mon, 9 May 2022 17:30:51 +0800 Subject: [PATCH 241/434] Fix the following issues: 1. HashMap non-thread-safe issus 2. Fix the same problem in pr621, but use a better implementation Add the following functions: 1. There is now a language cache inside getLanguage to prepare for different languages corresponding to different time zones where the accounts in the server are located --- src/main/java/emu/grasscutter/Config.java | 2 +- src/main/java/emu/grasscutter/Grasscutter.java | 9 +-------- src/main/java/emu/grasscutter/utils/Language.java | 14 +++++++++++--- src/main/java/emu/grasscutter/utils/Utils.java | 9 +++++++++ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 6473e2846..4ec16e0d1 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -22,7 +22,7 @@ public final class Config { public GameServerOptions GameServer = new GameServerOptions(); public DispatchServerOptions DispatchServer = new DispatchServerOptions(); public Locale LocaleLanguage = Locale.getDefault(); - public Locale DefaultLanguage = Locale.ENGLISH; + public Locale DefaultLanguage = Locale.US; public Boolean OpenStamina = true; public GameServerOptions getGameServerOptions() { diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index c593f5f13..f426d41af 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -149,14 +149,7 @@ public final class Grasscutter { public static void loadLanguage() { var locale = config.LocaleLanguage; - var languageTag = locale.toLanguageTag(); - - if (languageTag.equals("und")) { - Grasscutter.getLogger().error("Illegal locale language, using 'en-US' instead."); - language = Language.getLanguage("en-US"); - } else { - language = Language.getLanguage(languageTag); - } + language = Language.getLanguage(Utils.getLanguageCode(locale)); } public static void saveConfig() { diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 70e32e658..04bd352f7 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -6,12 +6,13 @@ import emu.grasscutter.Grasscutter; import javax.annotation.Nullable; import java.io.InputStream; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.Map; public final class Language { private final JsonObject languageData; - private final Map<String, String> cachedTranslations = new HashMap<>(); + private final Map<String, String> cachedTranslations = new ConcurrentHashMap<>(); + private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>(); /** * Creates a language instance from a code. @@ -19,7 +20,13 @@ public final class Language { * @return A language instance. */ public static Language getLanguage(String langCode) { - return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json"); + if (cachedLanguages.containsKey(langCode)) { + return cachedLanguages.get(langCode); + } + + var languageInst = new Language(langCode + ".json", Utils.getLanguageCode(Grasscutter.getConfig().DefaultLanguage) + ".json"); + cachedLanguages.put(langCode, languageInst); + return languageInst; } /** @@ -42,6 +49,7 @@ public final class Language { /** * Reads a file and creates a language instance. * @param fileName The name of the language file. + * @param fallback The name of the fallback language file. */ private Language(String fileName, String fallback) { @Nullable JsonObject languageData = null; diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 1d79c496e..764993255 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -9,6 +9,7 @@ import java.time.temporal.TemporalAdjusters; import java.util.HashMap; import java.util.Map; import java.util.Random; +import java.util.Locale; import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; @@ -306,4 +307,12 @@ public final class Utils { return map; } + + /** + * get language code from Locale + */ + public static String getLanguageCode(Locale locale) { + return String.format("%s-%s", locale.getLanguage(), locale.getCountry()); + } + } From 5a6512c5cf156f02e7c2c6ff6f7cb67734fd8072 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 01:41:20 -0700 Subject: [PATCH 242/434] No more fallen to death in god mode --- .../server/packet/recv/HandlerCombatInvocationsNotify.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 1ce089c26..04604ddc1 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -77,6 +77,9 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { } private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { + if (session.getPlayer().inGodmode()) { + return; + } // People have reported that after plunge attack (client sends a FIGHT instead of FALL_ON_GROUND) they will die // if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping again. // A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet. From fd3e59f33293b1992aaeb85afbe71f47021883b1 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Tue, 10 May 2022 01:48:27 -0700 Subject: [PATCH 243/434] Remove previous fix for falling to death in godmode This fix is more efficient --- .../server/packet/recv/HandlerCombatInvocationsNotify.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 04604ddc1..95171bec8 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -105,10 +105,6 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { if (cachedLandingSpeed < -28) { damageFactor = 1f; } - // Disable falling damage for players in god mode. - if (session.getPlayer() != null && session.getPlayer().inGodmode()) { - damageFactor = 0; - } float damage = maxHP * damageFactor; float newHP = currentHP - damage; if (newHP < 0) { From 8074f47c6a7c689a8e3557a03124f8f181d0045c Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 01:42:23 -0700 Subject: [PATCH 244/434] Stamina will be set to full if Stamina is disabled. --- .../StaminaManager/StaminaManager.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 1156993b1..937ba3b3f 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -190,17 +190,17 @@ public class StaminaManager { // Returns new stamina and sends PlayerPropNotify public int setStamina(GameSession session, String reason, int newStamina) { - if (Grasscutter.getConfig().OpenStamina) { - // set stamina - player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); - session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); - // notify updated - for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { - listener.getValue().onAfterUpdateStamina(reason, newStamina); - } - return newStamina; + if (!Grasscutter.getConfig().OpenStamina) { + newStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); } - return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); + // set stamina + player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); + session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); + // notify updated + for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { + listener.getValue().onAfterUpdateStamina(reason, newStamina); + } + return newStamina; } // Kills avatar, removes entity and sends notification. From 32154c2a555683d7d2006d5ae142e00c11d63407 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 02:03:18 -0700 Subject: [PATCH 245/434] Temporarily set statue auto use to 1 and 100%. --- .../java/emu/grasscutter/game/managers/SotSManager.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager.java index ed67c6a62..564663662 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager.java @@ -93,9 +93,14 @@ public class SotSManager { } public void refillSpringVolume() { - // TODO: max spring volume depends on level of the statues in Mondstadt and Liyue. + // Temporary: Max spring volume depends on level of the statues in Mondstadt and Liyue. Override until we have statue level. + // TODO: remove // https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000); + // Temporary: Auto enable 100% statue recovery until we can adjust statue settings in game + // TODO: remove + player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 100); + player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); long now = System.currentTimeMillis() / 1000; long secondsSinceLastUsed = now - player.getSpringLastUsed(); From bf3d6b3c6473c358d7740348b2fbcb6bf62a249d Mon Sep 17 00:00:00 2001 From: Bi Jiakai <bijk@xkool.org> Date: Tue, 10 May 2022 17:17:54 +0800 Subject: [PATCH 246/434] Fixed account delete can not delete all related data (#767) --- .../grasscutter/database/DatabaseHelper.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index bfcd006df..8f1de0bb9 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -16,6 +16,8 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; +import static com.mongodb.client.model.Filters.eq; + public final class DatabaseHelper { public static Account createAccount(String username) { return createAccountWithId(username, 0); @@ -101,17 +103,20 @@ public final class DatabaseHelper { // This should optimally be wrapped inside a transaction, to make sure an error thrown mid-way does not leave the // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... - // Delete mails, gacha records, items and avatars. - DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", target.getPlayerUid())).delete(); - DatabaseManager.getDatastore().find(GachaRecord.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); - DatabaseManager.getDatastore().find(GameItem.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); - DatabaseManager.getDatastore().find(Avatar.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); + // Delete Mail.class data + DatabaseManager.getDatabase().getCollection("mail").deleteMany(eq("ownerUid", target.getPlayerUid())); + // Delete Avatar.class data + DatabaseManager.getDatabase().getCollection("avatars").deleteMany(eq("ownerId", target.getPlayerUid())); + // Delete GachaRecord.class data + DatabaseManager.getDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); + // Delete GameItem.class data + DatabaseManager.getDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); // Delete friendships. // Here, we need to make sure to not only delete the deleted account's friendships, // but also all friendship entries for that account's friends. - DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("ownerId", target.getPlayerUid())).delete(); - DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("friendId", target.getPlayerUid())).delete(); + DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("ownerId", target.getPlayerUid())); + DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("friendId", target.getPlayerUid())); // Delete the player. DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete(); From d7b3ee10baf194e281bad4729042114fbdfec069 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 10 May 2022 17:01:22 +0800 Subject: [PATCH 247/434] Update zh-CN.json & fix typo --- src/main/resources/languages/zh-CN.json | 208 ++++++++++++------------ 1 file changed, 106 insertions(+), 102 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index f9e750438..5a773538d 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -2,47 +2,47 @@ "messages": { "game": { "port_bind": "游戏服务器已在端口 %s 上启动", - "connect": "客户端已连接至 %s", + "connect": "客户端 %s 已连接", "disconnect": "客户端 %s 已断开连接", "game_update_error": "游戏更新时发生错误", "command_error": "命令发生错误:" }, "dispatch": { "port_bind": "[Dispatch] 服务器已在端口 %s 上启动", - "request": "[Dispatch] 客户端 %s 请求: %s %s", + "request": "[Dispatch] 客户端 %s 请求:%s %s", "keystore": { "general_error": "[Dispatch] 加载 keystore 文件时发生错误!", - "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用预设的 keystore 密码...", + "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用默认密码...", "no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 服务器", "default_password": "[Dispatch] 默认的 keystore 密码加载成功。请考虑将 config.json 的默认密码设置为 123456" }, "no_commands_error": "此命令不适用于 Dispatch-only 模式", - "unhandled_request_error": "[Dispatch] 潜在的未处理请求 %s 请求:%s", + "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", "account": { "login_attempt": "[Dispatch] 客户端 %s 正在尝试登录", "login_success": "[Dispatch] 客户端 %s 已登录,UID为 %s", - "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用令牌登录", - "login_token_error": "[Dispatch] 客户端 %s 使用令牌登录失败", - "login_token_success": "[Dispatch] 客户端 %s 已通过令牌登录,UID为 %s", - "combo_token_success": "[Dispatch] 客户端 %s 交换令牌成功", - "combo_token_error": "[Dispatch] 客户端 %s 交换令牌失败", + "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录", + "login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败", + "login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID为 %s", + "combo_token_success": "[Dispatch] 客户端 %s 交换 token 成功", + "combo_token_error": "[Dispatch] 客户端 %s 交换 token 失败", "account_login_create_success": "[Dispatch] 客户端 %s 登录失败: 已注册UID为 %s 的账号", "account_login_create_error": "[Dispatch] 客户端 %s 登录失败:账号创建失败。", "account_login_exist_error": "[Dispatch] 客户端 %s 登录失败:账号不存在", "account_cache_error": "游戏账号缓存信息错误", - "session_key_error": "交换秘钥不符。", + "session_key_error": "会话密钥错误。", "username_error": "未找到此用户名。", "username_create_error": "未找到用户名,建立连接失败。" } }, "status": { - "free_software": "Grasscutter 是免费开源软件,遵循AGPL-3.0 license。如果您是付费购买的,那您已经被骗了。项目地址:Github:https://github.com/Grasscutters/Grasscutter", + "free_software": "Grasscutter 是免费开源软件,遵循 AGPL-3.0 license。如果你是付费购买的,那你已经被骗了。项目地址:https://github.com/Grasscutters/Grasscutter", "starting": "正在启动 Grasscutter...", "shutdown": "正在关闭...", "done": "加载完成!输入 \"help\" 查看命令列表", "error": "发生了一个错误。", "welcome": "欢迎使用 Grasscutter!珍惜这段美妙的旅途吧!", - "run_mode_error": "无效的服务器运行模式: %s。", + "run_mode_error": "无效的服务器运行模式:%s。", "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "create_resources": "正在创建 resources 目录...", "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。" @@ -52,18 +52,19 @@ "generic": { "not_specified": "没有指定命令。", "unknown_command": "未知的命令:%s", - "permission_error": "哼哼哼!您没有执行此命令的权限!请联系服务器管理员解决!", + "permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!", "console_execute_error": "此命令只能在服务器控制台执行呐~", "player_execute_error": "此命令只能在游戏内执行哦~", "command_exist_error": "这条命令……好像找不到呢?。", + "no_description_specified": "没有指定说明", "invalid": { "amount": "无效的数量.", "artifactId": "无效的圣遗物ID。", "avatarId": "无效的角色ID。", - "avatarLevel": "无效的角色等級。", + "avatarLevel": "无效的角色等级。", "entityId": "无效的实体ID。", "itemId": "无效的物品ID。", - "itemLevel": "无效的物品等級。", + "itemLevel": "无效的物品等级。", "itemRefinement": "无效的物品精炼等级。", "playerId": "无效的玩家ID。", "uid": "无效的UID。" @@ -71,16 +72,16 @@ }, "execution": { "uid_error": "无效的UID。", - "player_exist_error": "用户不存在。", + "player_exist_error": "玩家不存在。", "player_offline_error": "玩家已离线。", "item_id_error": "无效的物品ID。.", "item_player_exist_error": "无效的物品/玩家UID。", "entity_id_error": "无效的实体ID。", "player_exist_offline_error": "玩家不存在或已离线。", "argument_error": "无效的参数。", - "clear_target": "目标已清除.", + "clear_target": "目标已清除。", "set_target": "随后的的命令都会以@%s为预设。", - "need_target": "此命令需要一个目标 UID。添加 <@UID> 参数或使用 /target @UID 来设定持久目标。" + "need_target": "此命令需要一个目标 UID。添加 <@UID> 参数或使用 /target @UID 来指定默认目标。" }, "status": { "enabled": "已启用", @@ -89,13 +90,13 @@ "success": "成功" }, "account": { - "modify": "修改使用者账号", + "modify": "修改用户账号", "invalid": "无效的UID。", "exists": "账号已存在。", - "create": "已建立账号,UID 为 %s 。", + "create": "已创建账号,UID 为 %s 。", "delete": "账号已刪除。", "no_account": "账号不存在。", - "command_usage": "用法:account <create|delete> <username> [uid]", + "command_usage": "用法:account <create|delete> <用户名> [uid]", "description": "创建或删除账号。" }, "broadcast": { @@ -104,72 +105,72 @@ "description": "向所有玩家发送公告。" }, "changescene": { - "usage": "用法:changescene <scene id>", - "already_in_scene": "你已经在这个秘境中了。", - "success": "已切换至秘境 %s.", - "exists_error": "此秘境不存在。", - "description": "切换指定秘境。" + "usage": "用法:changescene <场景ID>", + "already_in_scene": "你已经在这个场景中了。", + "success": "已切换至场景 %s。", + "exists_error": "此场景不存在。", + "description": "切换指定场景。" }, "clear": { - "command_usage": "用法: clear <all|wp|art|mat>", - "weapons": "已将 %s 的武器清空。", - "artifacts": "已将 %s 的圣遗物清空。", - "materials": "已将 %s 的材料清空。", - "furniture": "已将 %s 的尘歌壶家具清空。", - "displays": "已清除 %s 的显示。", - "virtuals": "已将 %s 的所有货币和经验值清空。", - "everything": "已将 %s 的所有物品清空。", - "description": "从您的背包中删除所有未装备且已解锁的物品,包括稀有物品。" + "command_usage": "用法: clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", + "weapons": "已清除 %s 的武器。", + "artifacts": "已清除 %s 的圣遗物。", + "materials": "已清除 %s 的材料。", + "furniture": "已清除 %s 的尘歌壶家具。", + "displays": "已清空 %s 的屏幕。", + "virtuals": "已清除 %s 的所有货币和经验值。", + "everything": "已清除 %s 的所有物品。", + "description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品。" }, "coop": { - "usage": "用法:coop <playerId> <target playerId>", - "success": "已强制召唤 %s 到 %s的世界", - "description": "强制召唤指定用户到他人的世界。" + "usage": "用法:coop <玩家ID> <目标玩家ID>", + "success": "已强制传送 %s 到 %s 的世界", + "description": "强制传送指定用户到他人的世界。" }, "enter_dungeon": { - "usage": "用法:enterdungeon <dungeon id>", + "usage": "用法:enterdungeon <秘境ID>", "changed": "已进入秘境 %s", "not_found_error": "此秘境不存在。", "in_dungeon_error": "你已经在秘境中了。", "description": "进入指定秘境。" }, "giveAll": { - "usage": "用法:giveall [player] [amount]", + "usage": "用法:giveall [玩家] [数量]", "started": "正在给予全部物品...", - "success": "已给予全部物品。", + "success": "已给予 %s 全部物品。", "invalid_amount_or_playerId": "无效的数量/玩家ID。", "description": "给予所有物品。" }, "giveArtifact": { - "usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", + "usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]", "id_error": "无效的圣遗物ID。", "success": "已将 %s 给予 %s。", "description": "给予指定圣遗物。" }, "giveChar": { - "usage": "用法:givechar <player> <itemId|itemName> [amount]", - "given": "给予角色 %s 等级 %s 向UID %s.", + "usage": "用法:givechar <玩家> <角色ID|角色名> [数量]", + "given": "已将角色 %s (等级 %s) 给与 %s。", "invalid_avatar_id": "无效的角色ID。", - "invalid_avatar_level": "无效的角色等級。.", + "invalid_avatar_level": "无效的角色等级。.", "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。", "description": "给予指定角色。" }, "give": { - "usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]", - "refinement_only_applicable_weapons": "精炼等阶参数仅在给予武器时可用", - "refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。", + "usage": "用法:give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]", + "refinement_only_applicable_weapons": "只有武器可以设置精炼等级。", + "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", "given": "已将 %s 个 %s 给予 %s。", - "given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s", - "given_level": "已将 %s 等级 %s %s 个给予UID %s", + "given_with_level_and_refinement": "已将 %s (等级 %s, 精炼 %s) %s 个给予 %s", + "given_level": "已将 %s (等级 %s) %s 个给予 %s", "description": "给予指定物品。" }, "godmode": { - "success": "上帝模式已被设置为 %s 。 [用户:%s]", + "success": "%s 的无敌模式已被设置为 %s。", "description": "防止你受到伤害。" }, "heal": { - "success": "所有角色已被治疗。", - "description": "治疗所选队伍的角色。" + "success": "已经治疗所有角色。", + "description": "治疗场上队伍的角色。" }, "kick": { "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", @@ -177,28 +178,28 @@ "description": "从服务器内踢出指定玩家。" }, "kill": { - "usage": "用法:killall [playerUid] [sceneId]", + "usage": "用法:killall [玩家UID] [场景ID]", "scene_not_found_in_player_world": "未在玩家世界中找到此场景", - "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]", + "kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。", "description": "杀死所有怪物" }, "killCharacter": { - "usage": "用法:/killcharacter [playerId]", - "success": "已杀死 %s 目前使用的角色。", - "description": "杀死目前使用的角色" + "usage": "用法:/killcharacter [玩家ID]", + "success": "已杀死 %s 当前角色。", + "description": "杀死当前角色" }, "list": { "success": "目前在线人数:%s", "description": "查看所有玩家" }, "permission": { - "usage": "用法:permission <add|remove> <username> <permission>", - "add": "已设置权限。", + "usage": "用法:permission <add|remove> <用户名> <权限>", + "add": "权限已添加。", "has_error": "此玩家已拥有此权限!", "remove": "权限已移除。", "not_have_error": "此玩家未拥有权限!", - "account_error": "账号不存在!", - "description": "给予或移除指定玩家的权限。" + "account_error": "账号不存在。", + "description": "添加或移除指定玩家的权限。" }, "position": { "success": "坐标:%s, %s, %s\n场景ID:%s", @@ -206,74 +207,74 @@ }, "reload": { "reload_start": "正在重载配置文件和数据。", - "reload_done": "重载完毕。", + "reload_done": "重载完成。", "description": "重载配置文件和数据。" }, "resetConst": { "reset_all": "重置所有角色的命座。", - "success": "已重置 %s 的命座,重新登录后将会生效。", + "success": "已重置 %s 的命座,重新登录后生效。", "description": "重置当前角色的命之座,执行命令后需重新登录以生效。" }, "resetShopLimit": { - "usage": "用法:/resetshop <player id>", + "usage": "用法:/resetshop <玩家ID>", "description": "重置所选玩家的商店刷新时间。" }, "sendMail": { - "usage": "用法:give [player] <itemId|itemName> [amount]", - "user_not_exist": "ID '%s' 的使用者不存在。", - "start_composition": "发送邮件流程。\n请使用`/send <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。", + "usage": "用法:give [玩家] <物品ID|物品名称> [数量]", + "user_not_exist": "ID '%s' 的用户不存在。", + "start_composition": "发送邮件流程。\n请使用`/sendmail <标题>`前进到下一步。\n你可以在任何时间使用`/sendmail stop`来停止发送。", "templates": "邮件模板尚未实装...", - "invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <userId|all|help> [templateId]`", + "invalid_arguments": "无效的参数。\n指令使用方法 `/sendmail <用户ID|all|help> [模板ID]`", "send_cancel": "取消发送邮件", "send_done": "已将邮件发送给 %s!", "send_all_done": "邮件已发送给所有人!", "not_composition_end": "现在邮件发送未到最后阶段。\n请使用 `/sendmail %s` 继续发送邮件,或使用 `/sendmail stop` 来停止发送邮件。", "please_use": "请使用 `/sendmail %s`", - "set_title": "成功将邮件标题设置为 '%s'。\n使用 '/sendmail <content>' 来设置邮件内容。", - "set_contents": "成功将'%s'设置为邮件内容。\n使用 '/sendmail <发件人>' 来设置发件人。", - "set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <itemId|itemName|finish> [amount] [level]' 来添加附件。", - "send": "已添加 %s 个 %s (等級为 %s) 邮件附件。\n如果没有要继续添加的道具请使用 `/sendmail finish` 来完成邮件发送。", + "set_title": "成功将邮件标题设置为 '%s'。\n使用 '/sendmail <正文>' 来设置邮件内容。", + "set_contents": "成功将邮件内容设置为 '%s'。\n使用 '/sendmail <发件人>' 来设置发件人。", + "set_message_sender": "发件人已设置为 '%s'。\n使用 '/sendmail <物品ID|物品名称|finish> [数量] [等级]' 来添加附件。", + "send": "已添加 %s 个 %s (等级 %s) 邮件附件。\n如果没有要继续添加的附件请使用 `/sendmail finish` 来发送邮件。", "invalid_arguments_please_use": "错误的参数 \n请使用 `/sendmail %s`", "title": "<标题>", "message": "<正文>", "sender": "<发件人>", - "arguments": "<itemId|itemName|finish> [数量] [等级]", + "arguments": "<物品ID|物品名称|finish> [数量] [等级]", "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。", - "description": "向指定用户发送邮件。 此命令的用法可根据附加的参数而变化。" + "description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化。" }, "sendMessage": { - "usage": "用法:sendmessage <player> <message>", + "usage": "用法:sendmessage <玩家> <消息>", "success": "消息已发送。", "description": "向指定玩家发送消息" }, "setFetterLevel": { - "usage": "用法:setfetterlevel <level>", + "usage": "用法:setfetterlevel <好感度等级>", "range_error": "好感度等级必须在 0 到 10 之间。", "success": "好感度已设置为 %s 级", "level_error": "无效的好感度等级。", "description": "设置当前角色的好感度等级。" }, "setStats": { - "usage_console": "用法:setstats|stats @<UID> <stat> <value>", - "usage_ingame": "用法:setstats|stats [@UID] <stat> <value>", - "help_message": "\n\t可使用的数据类型:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(cont.) 元素伤害:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)(cont.) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", - "value_error": "无效的数据值。", + "usage_console": "用法:setstats|stats @<UID> <属性> <数值>", + "usage_ingame": "用法:setstats|stats [@UID] <属性> <数值>", + "help_message": "\n\t可更改的属性列表:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(续) 元素增伤:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)\n\t(续) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", + "value_error": "无效的属性值。", "uid_error": "无效的UID。", "player_error": "玩家不存在或已离线。", - "set_self": "%s 已经设置为 %s。", - "set_for_uid": "%s 的使用者 %s 更改为 %s。", - "set_max_hp": "最大生命值更改为 %s。", + "set_self": "%s 已设为 %s。", + "set_for_uid": "将 %s (来自 %s) 设置为 %s。", + "set_max_hp": "最大生命值已设为 %s。", "description": "设置当前角色的属性。" }, "setWorldLevel": { - "usage": "用法:setworldlevel <level>", + "usage": "用法:setworldlevel <等级>", "value_error": "世界等级必须设置在0-8之间。", - "success": "已将世界等级设为%s。", + "success": "已将世界等级设为 %s。", "invalid_world_level": "无效的世界等级。", "description": "设置世界等级,执行命令后需重新登录以生效。" }, "spawn": { - "usage": "用法:spawn <entityId> [amount] [level(仅限怪物]", + "usage": "用法:spawn <实体ID> [数量] [等级(仅怪物)]", "success": "已生成 %s 个 %s。", "description": "在你附近生成一个生物。" }, @@ -282,14 +283,14 @@ "description": "停止服务器" }, "talent": { - "usage_1": "设置天赋等级:/talent set <talentID> <value>", - "usage_2": "另一种设置天赋等级的命令使用方法:/talent <n or e or q> <value>", - "usage_3": "获取天赋ID指令用法:/talent getid", + "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", + "usage_2": "另一种设置天赋等级的方法:/talent <n (普攻) | e (元素战技) | q (元素爆发)> <数值>", + "usage_3": "获取天赋ID:/talent getid", "lower_16": "无效的天赋等级,天赋等级应小于等于15。", "set_id": "将天赋等级设为 %s。", "set_atk": "将普通攻击等级设为 %s。", - "set_e": "设定元素战技等级为 %s。", - "set_q": "设定元素爆发等级为 %s。", + "set_e": "设置元素战技等级为 %s。", + "set_q": "设置元素爆发等级为 %s。", "invalid_skill_id": "无效的技能ID。", "set_this": "将天赋等级设为 %s。", "invalid_level": "无效的天赋等级。", @@ -299,33 +300,36 @@ "description": "设置当前角色的天赋等级。" }, "teleportAll": { - "success": "已将全部玩家传送到你的位置", - "error": "命令仅限处于多人游戏状态下使用。", + "success": "已将所有玩家传送到你的位置", + "error": "你只能在多人游戏状态下执行此命令。", "description": "将你世界中的所有玩家传送到你所在的位置。" }, "teleport": { - "usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]", - "usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]", + "usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]", + "usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]", "specify_player_id": "你必须指定一个玩家ID。", "invalid_position": "无效的位置。", "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s", "description": "改变指定玩家的位置。" }, + "tower": { + "unlock_done": "深境回廊的所有层已全部解锁。" + }, "weather": { - "usage": "用法:weather <weatherId> [climateId]", - "success": "已将当前天气设定为 %s,气候为 %s。", + "usage": "用法:weather <天气ID> [气候ID]", + "success": "已更改天气为 %s,气候为 %s。", "invalid_id": "无效的天气ID。", - "description": "改变天气" + "description": "更改天气" }, "drop": { - "command_usage": "用法:drop <itemId|itemName> [amount]", - "success": "已将 %s x %s 丟在附近。", + "command_usage": "用法:drop <物品ID|物品名称> [数量]", + "success": "已丢下 %s 个 %s。", "description": "在你附近丢一个物品。" }, "help": { "usage": "用法:", "aliases": "別名:", - "available_commands": "可用指令:", + "available_commands": "可用命令:", "description": "发送帮助信息或显示指定命令的信息。" }, "restart": { From bd5e7c68fd098fee85fb24721d7dbd5ecc3b4287 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 10 May 2022 17:49:50 +0800 Subject: [PATCH 248/434] Update zh-CN.json --- src/main/resources/languages/zh-CN.json | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 5a773538d..8736c719a 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -12,9 +12,9 @@ "request": "[Dispatch] 客户端 %s 请求:%s %s", "keystore": { "general_error": "[Dispatch] 加载 keystore 文件时发生错误!", - "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用默认密码...", - "no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 服务器", - "default_password": "[Dispatch] 默认的 keystore 密码加载成功。请考虑将 config.json 的默认密码设置为 123456" + "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用 keystore 默认密码...", + "no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 模式", + "default_password": "[Dispatch] keystore 默认密码加载成功。请考虑将 config.json 的默认密码设置为 123456" }, "no_commands_error": "此命令不适用于 Dispatch-only 模式", "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", @@ -23,10 +23,10 @@ "login_success": "[Dispatch] 客户端 %s 已登录,UID为 %s", "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录", "login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败", - "login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID为 %s", + "login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID 为 %s", "combo_token_success": "[Dispatch] 客户端 %s 交换 token 成功", "combo_token_error": "[Dispatch] 客户端 %s 交换 token 失败", - "account_login_create_success": "[Dispatch] 客户端 %s 登录失败: 已注册UID为 %s 的账号", + "account_login_create_success": "[Dispatch] 客户端 %s 登录失败: 已注册 UID 为 %s 的账号", "account_login_create_error": "[Dispatch] 客户端 %s 登录失败:账号创建失败。", "account_login_exist_error": "[Dispatch] 客户端 %s 登录失败:账号不存在", "account_cache_error": "游戏账号缓存信息错误", @@ -58,7 +58,7 @@ "command_exist_error": "这条命令……好像找不到呢?。", "no_description_specified": "没有指定说明", "invalid": { - "amount": "无效的数量.", + "amount": "无效的数量。", "artifactId": "无效的圣遗物ID。", "avatarId": "无效的角色ID。", "avatarLevel": "无效的角色等级。", @@ -74,7 +74,7 @@ "uid_error": "无效的UID。", "player_exist_error": "玩家不存在。", "player_offline_error": "玩家已离线。", - "item_id_error": "无效的物品ID。.", + "item_id_error": "无效的物品ID。", "item_player_exist_error": "无效的物品/玩家UID。", "entity_id_error": "无效的实体ID。", "player_exist_offline_error": "玩家不存在或已离线。", @@ -93,8 +93,8 @@ "modify": "修改用户账号", "invalid": "无效的UID。", "exists": "账号已存在。", - "create": "已创建账号,UID 为 %s 。", - "delete": "账号已刪除。", + "create": "已创建账号,UID 为 %s。", + "delete": "账号已删除。", "no_account": "账号不存在。", "command_usage": "用法:account <create|delete> <用户名> [uid]", "description": "创建或删除账号。" @@ -151,7 +151,7 @@ "usage": "用法:givechar <玩家> <角色ID|角色名> [数量]", "given": "已将角色 %s (等级 %s) 给与 %s。", "invalid_avatar_id": "无效的角色ID。", - "invalid_avatar_level": "无效的角色等级。.", + "invalid_avatar_level": "无效的角色等级。", "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。", "description": "给予指定角色。" }, @@ -170,7 +170,7 @@ }, "heal": { "success": "已经治疗所有角色。", - "description": "治疗场上队伍的角色。" + "description": "治疗当前队伍的角色。" }, "kick": { "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", @@ -289,8 +289,8 @@ "lower_16": "无效的天赋等级,天赋等级应小于等于15。", "set_id": "将天赋等级设为 %s。", "set_atk": "将普通攻击等级设为 %s。", - "set_e": "设置元素战技等级为 %s。", - "set_q": "设置元素爆发等级为 %s。", + "set_e": "将元素战技等级设为 %s。", + "set_q": "将元素爆发等级设为 %s。", "invalid_skill_id": "无效的技能ID。", "set_this": "将天赋等级设为 %s。", "invalid_level": "无效的天赋等级。", @@ -324,11 +324,11 @@ "drop": { "command_usage": "用法:drop <物品ID|物品名称> [数量]", "success": "已丢下 %s 个 %s。", - "description": "在你附近丢一个物品。" + "description": "在你附近丢下一个物品。" }, "help": { "usage": "用法:", - "aliases": "別名:", + "aliases": "别名:", "available_commands": "可用命令:", "description": "发送帮助信息或显示指定命令的信息。" }, From c4078a505ae8a29897c076efab87836b3fe1d15f Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 10 May 2022 17:58:32 +0800 Subject: [PATCH 249/434] Update zh-CN.json --- src/main/resources/languages/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 8736c719a..f6a2de0ea 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -112,7 +112,7 @@ "description": "切换指定场景。" }, "clear": { - "command_usage": "用法: clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", + "command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", "weapons": "已清除 %s 的武器。", "artifacts": "已清除 %s 的圣遗物。", "materials": "已清除 %s 的材料。", From 55b91ad150c4a8a74f80c2d331ef0527396dd6ec Mon Sep 17 00:00:00 2001 From: Kinesis <CCasusensa@users.noreply.github.com> Date: Tue, 10 May 2022 19:33:04 +0800 Subject: [PATCH 250/434] fixed KillCharacter help command description typo case --- .../emu/grasscutter/command/commands/KillCharacterCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 7435c6d5f..99a57e122 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -13,7 +13,7 @@ import java.util.List; import static emu.grasscutter.utils.Language.translate; -@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others", description = "commands.killcharacter.description") +@Command(label = "killcharacter", usage = "killcharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others", description = "commands.killCharacter.description") public final class KillCharacterCommand implements CommandHandler { @Override From f1aa930ef585fbb3c0db730886b9e2b57618af47 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 05:02:19 -0700 Subject: [PATCH 251/434] Add UTF8 charset in dispatch server HTML template. --- .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 7e4b1655e..f7f14019f 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -252,14 +252,14 @@ public final class DispatchServer { else config.enableCorsForAllOrigins(); } }); - httpServer.get("/", (req, res) -> res.send(translate("messages.status.welcome"))); + httpServer.get("/", (req, res) -> res.send("<!doctype html><html><head><meta charset=\"utf8\"></head><body>" + translate("messages.status.welcome") + "</body></html>")); httpServer.raw().error(404, ctx -> { if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url())); } ctx.contentType("text/html"); - ctx.result("<!doctype html><html lang=\"en\"><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything. + ctx.result("<!doctype html><html><head><meta charset=\"utf8\"></head><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything. }); // Authentication Handler From 812435b3331f31d6dd53021345fcbad9a8907744 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 04:15:47 -0700 Subject: [PATCH 252/434] Talent moving stamina cost --- .../game/ability/AbilityManager.java | 17 +- .../managers/StaminaManager/Consumption.java | 17 +- .../StaminaManager/ConsumptionType.java | 26 +- .../StaminaManager/StaminaManager.java | 384 +++++++++++------- .../game/props/PlayerProperty.java | 2 +- .../recv/HandlerEvtDoSkillSuccNotify.java | 11 +- 6 files changed, 286 insertions(+), 171 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java index d1ae388ea..b897f3df5 100644 --- a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java +++ b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java @@ -2,27 +2,22 @@ package emu.grasscutter.game.ability; import com.google.protobuf.InvalidProtocolBufferException; -import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; -import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; -import emu.grasscutter.net.proto.AbilityInvokeArgumentOuterClass.AbilityInvokeArgument; import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange; import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap; -import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType; +import emu.grasscutter.net.proto.AbilityMixinCostStaminaOuterClass.AbilityMixinCostStamina; import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry; import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; public class AbilityManager { private Player player; @@ -138,13 +133,9 @@ public class AbilityManager { } } - private void handleMixinCostStamina(AbilityInvokeEntry invoke) { - // Not the right way of doing this - if (Grasscutter.getConfig().OpenStamina) { - // getPlayer().getStaminaManager().updateStamina(getPlayer().getSession(), -450); - // TODO - // set flag in stamina/movement manager that specifies the player is currently using an alternate sprint - } + private void handleMixinCostStamina(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { + AbilityMixinCostStamina costStamina = AbilityMixinCostStamina.parseFrom((invoke.getAbilityData())); + getPlayer().getStaminaManager().handleMixinCostStamina(costStamina.getIsSwim()); } private void handleGenerateElemBall(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java index 23eb44be9..a6185f063 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java @@ -1,15 +1,18 @@ package emu.grasscutter.game.managers.StaminaManager; public class Consumption { - public ConsumptionType consumptionType; - public int amount; + public ConsumptionType type = ConsumptionType.None; + public int amount = 0; - public Consumption(ConsumptionType ct, int a) { - consumptionType = ct; - amount = a; + public Consumption(ConsumptionType type, int amount) { + this.type = type; + this.amount = amount; } - public Consumption(ConsumptionType ct) { - this(ct, ct.amount); + public Consumption(ConsumptionType type) { + this(type, type.amount); + } + + public Consumption() { } } diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java index 9afb2171c..feb42d14e 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java @@ -4,23 +4,29 @@ public enum ConsumptionType { None(0), // consume - CLIMB_START(-500), CLIMBING(-150), + CLIMB_START(-500), CLIMB_JUMP(-2500), - SPRINT(-1800), DASH(-360), - FLY(-60), - SWIM_DASH_START(-20), - SWIM_DASH(-204), - SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height. FIGHT(0), // See StaminaManager.getFightConsumption() + FLY(-60), + // Slow swimming is handled per movement, not per second. + // Arm movement frequency depends on gender/age/height. + // TODO: Instead of cost -80 per tick, find a proper way to calculate cost. + SKIFF(-300), // TODO: Get real value + SPRINT(-1800), + SWIM_DASH_START(-20), + SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick + SWIMMING(-80), + TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick + TALENT_DASH_START(-1000), // restore - STANDBY(500), + POWERED_FLY(500), // TODO: Get real value + POWERED_SKIFF(2000), // TODO: Get real value RUN(500), - WALK(500), - STANDBY_MOVE(500), - POWERED_FLY(500); + STANDBY(500), + WALK(500); public final int amount; diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 937ba3b3f..cde40ee57 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -8,7 +8,6 @@ import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; -import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; @@ -16,13 +15,94 @@ import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.Position; +import org.jetbrains.annotations.NotNull; import java.lang.Math; import java.util.*; public class StaminaManager { + + // TODO: Skiff state detection? private final Player player; - private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); + private final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{ + put("CLIMB", new HashSet<>(List.of( + MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover + MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY + ))); + put("DASH", new HashSet<>(List.of( + MotionState.MOTION_DANGER_DASH, // sustained + MotionState.MOTION_DASH // sustained + ))); + put("FLY", new HashSet<>(List.of( + MotionState.MOTION_FLY, // sustained + MotionState.MOTION_FLY_FAST, // sustained + MotionState.MOTION_FLY_SLOW, // sustained + MotionState.MOTION_POWERED_FLY // sustained, recover + ))); + put("RUN", new HashSet<>(List.of( + MotionState.MOTION_DANGER_RUN, // sustained, recover + MotionState.MOTION_RUN // sustained, recover + ))); + put("SKIFF", new HashSet<>(List.of( + MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding + MotionState.MOTION_SKIFF_DASH, // NOT OBSERVED even when dashing + MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing + MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover + ))); + put("STANDBY", new HashSet<>(List.of( + MotionState.MOTION_DANGER_STANDBY_MOVE, // sustained, recover + MotionState.MOTION_DANGER_STANDBY, // sustained, recover + MotionState.MOTION_LADDER_TO_STANDBY, // NOT OBSERVED + MotionState.MOTION_STANDBY_MOVE, // sustained, recover + MotionState.MOTION_STANDBY // sustained, recover + ))); + put("SWIM", new HashSet<>(List.of( + MotionState.MOTION_SWIM_IDLE, // sustained + MotionState.MOTION_SWIM_DASH, // immediate and sustained + MotionState.MOTION_SWIM_JUMP, // NOT OBSERVED + MotionState.MOTION_SWIM_MOVE // sustained + ))); + put("WALK", new HashSet<>(List.of( + MotionState.MOTION_DANGER_WALK, // sustained, recover + MotionState.MOTION_WALK // sustained, recover + ))); + put("OTHER", new HashSet<>(List.of( + MotionState.MOTION_CLIMB_JUMP, // cost only once if repeated without switching state + MotionState.MOTION_DASH_BEFORE_SHAKE, // immediate one time sprint charge. + MotionState.MOTION_FIGHT, // immediate, if sustained then subsequent will be MOTION_NOTIFY + MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY, // immediate, observed when RUN/WALK->CLIMB + MotionState.MOTION_NOTIFY, // can be either cost or recover - check previous state and check skill casting + MotionState.MOTION_SIT_IDLE, // sustained, recover + MotionState.MOTION_JUMP // recover + ))); + put("NOCOST_NORECOVER", new HashSet<>(List.of( + MotionState.MOTION_LADDER_SLIP, // NOT OBSERVED + MotionState.MOTION_SLIP, // sustained, no cost no recover + MotionState.MOTION_FLY_IDLE // NOT OBSERVED + ))); + put("IGNORE", new HashSet<>(List.of( + // these states have no impact on stamina + MotionState.MOTION_CROUCH_IDLE, + MotionState.MOTION_CROUCH_MOVE, + MotionState.MOTION_CROUCH_ROLL, + MotionState.MOTION_DESTROY_VEHICLE, + MotionState.MOTION_FALL_ON_GROUND, + MotionState.MOTION_FOLLOW_ROUTE, + MotionState.MOTION_FORCE_SET_POS, + MotionState.MOTION_GO_UPSTAIRS, + MotionState.MOTION_JUMP_OFF_WALL, + MotionState.MOTION_LADDER_IDLE, + MotionState.MOTION_LADDER_MOVE, + MotionState.MOTION_LAND_SPEED, + MotionState.MOTION_MOVE_FAIL_ACK, + MotionState.MOTION_NONE, + MotionState.MOTION_NUM, + MotionState.MOTION_QUEST_FORCE_DRAG, + MotionState.MOTION_RESET, + MotionState.MOTION_STANDBY_TO_LADDER, + MotionState.MOTION_WATERFALL + ))); + }}; public final static int GlobalMaximumStamina = 24000; private Position currentCoordinates = new Position(0, 0, 0); @@ -33,70 +113,23 @@ public class StaminaManager { private GameSession cachedSession = null; private GameEntity cachedEntity = null; private int staminaRecoverDelay = 0; + private final HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>(); + private final HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>(); + private int lastSkillId = 0; + private int lastSkillCasterId = 0; + private boolean lastSkillFirstTick = true; - private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>(); - private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>(); public StaminaManager(Player player) { this.player = player; - - MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( - MotionState.MOTION_SWIM_MOVE, - MotionState.MOTION_SWIM_IDLE, - MotionState.MOTION_SWIM_DASH, - MotionState.MOTION_SWIM_JUMP - ))); - - MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList( - MotionState.MOTION_STANDBY, - MotionState.MOTION_STANDBY_MOVE, - MotionState.MOTION_DANGER_STANDBY, - MotionState.MOTION_DANGER_STANDBY_MOVE, - MotionState.MOTION_LADDER_TO_STANDBY, - MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY - ))); - - MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList( - MotionState.MOTION_CLIMB, - MotionState.MOTION_CLIMB_JUMP, - MotionState.MOTION_STANDBY_TO_CLIMB, - MotionState.MOTION_LADDER_IDLE, - MotionState.MOTION_LADDER_MOVE, - MotionState.MOTION_LADDER_SLIP, - MotionState.MOTION_STANDBY_TO_LADDER - ))); - - MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList( - MotionState.MOTION_FLY, - MotionState.MOTION_FLY_IDLE, - MotionState.MOTION_FLY_SLOW, - MotionState.MOTION_FLY_FAST, - MotionState.MOTION_POWERED_FLY - ))); - - MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList( - MotionState.MOTION_DASH, - MotionState.MOTION_DANGER_DASH, - MotionState.MOTION_DASH_BEFORE_SHAKE, - MotionState.MOTION_RUN, - MotionState.MOTION_DANGER_RUN, - MotionState.MOTION_WALK, - MotionState.MOTION_DANGER_WALK - ))); - - MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( - MotionState.MOTION_FIGHT - ))); - - MotionStatesCategorized.put("SKIFF", new HashSet<>(Arrays.asList( - MotionState.MOTION_SKIFF_BOARDING, - MotionState.MOTION_SKIFF_NORMAL, - MotionState.MOTION_SKIFF_DASH, - MotionState.MOTION_SKIFF_POWERED_DASH - ))); } - // Listeners + // Accessors + + public void setSkillCast(int skillId, int skillCasterId) { + lastSkillId = skillId; + lastSkillCasterId = skillCasterId; + } public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) { if (beforeUpdateStaminaListeners.containsKey(listenerName)) { @@ -146,17 +179,17 @@ public class StaminaManager { } // notify will update for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { - Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.consumptionType.toString(), consumption); - if ((overriddenConsumption.consumptionType != consumption.consumptionType) && (overriddenConsumption.amount != consumption.amount)) { + Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption); + if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) { Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" + - consumption.consumptionType.toString() + ", " + consumption.amount + ") overridden to relative(" + - consumption.consumptionType.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); + consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" + + consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); return currentStamina; } } int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + - (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," + + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," + consumption.amount + ")"); int newStamina = currentStamina + consumption.amount; if (newStamina < 0) { @@ -164,7 +197,7 @@ public class StaminaManager { } else if (newStamina > playerMaxStamina) { newStamina = playerMaxStamina; } - return setStamina(session, consumption.consumptionType.toString(), newStamina); + return setStamina(session, consumption.type.toString(), newStamina); } public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) { @@ -236,11 +269,24 @@ public class StaminaManager { // External trigger handler - public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) { - handleImmediateStamina(session, notify); + public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { + // Ignore if skill not cast by not current active + if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) { + return; + } + setSkillCast(skillId, casterId); + handleImmediateStamina(session, skillId); } - public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) { + public void handleMixinCostStamina(boolean isSwim) { + // Talent moving and claymore avatar charged attack duration + // Grasscutter.getLogger().trace("abilityMixinCostStamina: isSwim: " + isSwim); + if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) { + handleImmediateStamina(cachedSession, lastSkillId); + } + } + + public void handleCombatInvocationsNotify(@NotNull GameSession session, @NotNull EntityMoveInfo moveInfo, @NotNull GameEntity entity) { // cache info for later use in SustainedStaminaHandler tick cachedSession = session; cachedEntity = entity; @@ -252,20 +298,25 @@ public class StaminaManager { return; } currentState = motionState; + // Grasscutter.getLogger().trace("" + currentState); Vector posVector = motionInfo.getPos(); Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { currentCoordinates = newPos; } startSustainedStaminaHandler(); - handleImmediateStamina(session, motionInfo, motionState, entity); + handleImmediateStamina(session, motionState); } // Internal handler - private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState, - GameEntity entity) { + private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) { switch (motionState) { + case MOTION_CLIMB: + if (currentState != MotionState.MOTION_CLIMB) { + updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START)); + } + break; case MOTION_DASH_BEFORE_SHAKE: if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT)); @@ -284,8 +335,10 @@ public class StaminaManager { } } - private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) { - Consumption consumption = getFightConsumption(notify.getSkillId()); + private void handleImmediateStamina(GameSession session, int skillId) { + // Non-claymore avatar attacks + // TODO: differentiate charged vs normal attack + Consumption consumption = getFightConsumption(skillId); updateStaminaRelative(session, consumption); } @@ -298,32 +351,45 @@ public class StaminaManager { Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " + (currentStamina >= maxStamina) + ", recalculate stamina"); - Consumption consumption = new Consumption(ConsumptionType.None); + Consumption consumption; if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - consumption = getClimbSustainedConsumption(); - } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { - consumption = getSwimSustainedConsumptions(); - } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - consumption = getRunWalkDashSustainedConsumption(); + consumption = getClimbConsumption(); + } else if (MotionStatesCategorized.get("DASH").contains(currentState)) { + consumption = getDashConsumption(); } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - consumption = getFlySustainedConsumption(); + consumption = getFlyConsumption(); + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + consumption = new Consumption(ConsumptionType.RUN); + } else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) { + consumption = getSkiffConsumption(); } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - consumption = getStandSustainedConsumption(); + consumption = new Consumption(ConsumptionType.STANDBY); + } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { + consumption = getSwimConsumptions(); + } else if (MotionStatesCategorized.get("WALK").contains((currentState))) { + consumption = new Consumption(ConsumptionType.WALK); + } else if (MotionStatesCategorized.get("OTHER").contains((currentState))) { + consumption = getOtherConsumptions(); + } else { + // ignore + return; } - - /* + if (consumption.amount < 0) { + /* Do not apply reduction factor when recovering stamina TODO: Reductions that apply to all motion types: Elemental Resonance Wind: -15% Skills - Diona E: -10% while shield lasts - Barbara E: -12% while lasts - */ + Diona E: -10% while shield lasts - applies to SP+MP + Barbara E: -12% while lasts - applies to SP+MP + */ + } + // Delay 2 seconds before starts recovering stamina if (cachedSession != null) { if (consumption.amount < 0) { staminaRecoverDelay = 0; } - if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { + if (consumption.amount > 0 && consumption.type != ConsumptionType.POWERED_FLY) { // For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. if (staminaRecoverDelay < 10) { // For others recover after 2 seconds (10 ticks) - as official server does. @@ -384,91 +450,137 @@ public class StaminaManager { */ // TODO: Currently only handling Ayaka and Mona's talent moving initial costs. - Consumption consumption = new Consumption(ConsumptionType.None); - HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ - put(10013, -1000); // Kamisato Ayaka - put(10413, -1000); // Mona - }}; - if (fightingCost.containsKey(skillCasting)) { - consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting)); - } - return consumption; - } + Consumption consumption = new Consumption(); - private Consumption getClimbSustainedConsumption() { - Consumption consumption = new Consumption(ConsumptionType.None); - if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) { - consumption = new Consumption(ConsumptionType.CLIMBING); - if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { - consumption = new Consumption(ConsumptionType.CLIMB_START); + // Talent moving + HashMap<Integer, List<Consumption>> talentMovementConsumptions = new HashMap<>() {{ + // List[0] = initial cost, [1] = sustained cost. Sustained costs are divided by 3 per second as MixinStaminaCost is triggered at 3Hz. + put(10013, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Kamisato Ayaka + put(10413, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Mona + }}; + if (talentMovementConsumptions.containsKey(skillCasting)) { + if (lastSkillFirstTick) { + consumption = talentMovementConsumptions.get(skillCasting).get(1); + } else { + lastSkillFirstTick = false; + consumption = talentMovementConsumptions.get(skillCasting).get(0); } } - // TODO: Foods + // TODO: Claymore avatar charged attack + // HashMap<Integer, Integer> fightConsumptions = new HashMap<>(); + + // TODO: Non-claymore avatar charged attack + return consumption; } - private Consumption getSwimSustainedConsumptions() { + private Consumption getClimbConsumption() { + Consumption consumption = new Consumption(); + if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) { + consumption.type = ConsumptionType.CLIMBING; + consumption.amount = ConsumptionType.CLIMBING.amount; + } + // Climbing specific reductions + // TODO: create a food cost reduction map + HashMap<Integer, Float> foodReductionMap = new HashMap<>() {{ + // TODO: get real talent id + put(0, 0.8f); // Sample food + }}; + consumption.amount *= getFoodCostReductionFactor(foodReductionMap); + + HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ + // TODO: get real talent id + put(0, 0.8f); // Xiao + }}; + consumption.amount *= getTalentCostReductionFactor(talentReductionMap); + return consumption; + } + + private Consumption getSwimConsumptions() { handleDrowning(); - Consumption consumption = new Consumption(ConsumptionType.None); + Consumption consumption = new Consumption(); if (currentState == MotionState.MOTION_SWIM_MOVE) { - consumption = new Consumption(ConsumptionType.SWIMMING); + consumption.type = ConsumptionType.SWIMMING; + consumption.amount = ConsumptionType.SWIMMING.amount; } if (currentState == MotionState.MOTION_SWIM_DASH) { - consumption = new Consumption(ConsumptionType.SWIM_DASH); + consumption.type = ConsumptionType.SWIM_DASH; + consumption.amount = ConsumptionType.SWIM_DASH.amount; } + // Reductions + HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ + // TODO: get real talent id + put(0, 0.8f); // Beidou + put(1, 0.8f); // Sangonomiya Kokomi + }}; + consumption.amount *= getTalentCostReductionFactor(talentReductionMap); return consumption; } - private Consumption getRunWalkDashSustainedConsumption() { - Consumption consumption = new Consumption(ConsumptionType.None); + private Consumption getDashConsumption() { + Consumption consumption = new Consumption(); if (currentState == MotionState.MOTION_DASH) { - consumption = new Consumption(ConsumptionType.DASH); - // TODO: Foods - } - if (currentState == MotionState.MOTION_RUN) { - consumption = new Consumption(ConsumptionType.RUN); - } - if (currentState == MotionState.MOTION_WALK) { - consumption = new Consumption(ConsumptionType.WALK); + consumption.type = ConsumptionType.DASH; + consumption.amount = ConsumptionType.DASH.amount; + // TODO: Dashing specific reductions + // Foods: } return consumption; } - private Consumption getFlySustainedConsumption() { + private Consumption getFlyConsumption() { // POWERED_FLY, e.g. wind tunnel if (currentState == MotionState.MOTION_POWERED_FLY) { return new Consumption(ConsumptionType.POWERED_FLY); } Consumption consumption = new Consumption(ConsumptionType.FLY); - // Talent - HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ + // Passive Talents + HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ put(212301, 0.8f); // Amber put(222301, 0.8f); // Venti }}; + consumption.amount *= getTalentCostReductionFactor(talentReductionMap); + // TODO: Foods + return consumption; + } + + private Consumption getSkiffConsumption() { + // POWERED_SKIFF, e.g. wind tunnel + if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) { + return new Consumption(ConsumptionType.POWERED_SKIFF); + } + Consumption consumption = new Consumption(ConsumptionType.SKIFF); + // No known reduction for skiffing. + return consumption; + } + + private Consumption getOtherConsumptions() { + // TODO: Add logic + return new Consumption(); + } + + // Reduction getter + + private float getTalentCostReductionFactor(HashMap<Integer, Float> talentReductionMap) { + // All known talents reductions are not stackable float reduction = 1; for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) { for (int skillId : entity.getAvatar().getProudSkillList()) { - if (glidingCostReduction.containsKey(skillId)) { - float potentialLowerReduction = glidingCostReduction.get(skillId); + if (talentReductionMap.containsKey(skillId)) { + float potentialLowerReduction = talentReductionMap.get(skillId); if (potentialLowerReduction < reduction) { reduction = potentialLowerReduction; } } } } - consumption.amount *= reduction; - // TODO: Foods - return consumption; + return reduction; } - private Consumption getStandSustainedConsumption() { - Consumption consumption = new Consumption(ConsumptionType.None); - if (currentState == MotionState.MOTION_STANDBY) { - consumption = new Consumption(ConsumptionType.STANDBY); - } - if (currentState == MotionState.MOTION_STANDBY_MOVE) { - consumption = new Consumption(ConsumptionType.STANDBY_MOVE); - } - return consumption; + private float getFoodCostReductionFactor(HashMap<Integer, Float> foodReductionMap) { + // All known food reductions are not stackable + // TODO: Check consumed food (buff?) and return proper factor + float reduction = 1; + return reduction; } } diff --git a/src/main/java/emu/grasscutter/game/props/PlayerProperty.java b/src/main/java/emu/grasscutter/game/props/PlayerProperty.java index 3cb67d9bb..85a9456cf 100644 --- a/src/main/java/emu/grasscutter/game/props/PlayerProperty.java +++ b/src/main/java/emu/grasscutter/game/props/PlayerProperty.java @@ -30,7 +30,7 @@ public enum PlayerProperty { // his gems and then got a money refund, so negative is allowed. PROP_PLAYER_SCOIN (10016), // Mora [0, +inf) PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval] - PROP_IS_MP_MODE_AVAILABLE (10018), // Are you not in a quest or something that disables MP? [0, 1] + PROP_IS_MP_MODE_AVAILABLE (10018), // 0 if in quest or something that disables MP [0, 1] PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8] PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf) PROP_PLAYER_WAIT_SUB_HCOIN (10022), diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java index 705341fa0..36aa733c2 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -4,7 +4,9 @@ import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.Position; @Opcodes(PacketOpcodes.EvtDoSkillSuccNotify) public class HandlerEvtDoSkillSuccNotify extends PacketHandler { @@ -12,9 +14,10 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); - // TODO: Will be used for deducting stamina for charged skills. - - session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, notify); + int skillId = notify.getSkillId(); + int casterId = notify.getCasterId(); + Vector forwardVector = notify.getForward(); + Position forward = new Position(forwardVector.getX(), forwardVector.getY(), forwardVector.getZ()); + session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId); } - } From af5b7c6eb6f726706d15a6f038fc894cedd336ea Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 04:23:08 -0700 Subject: [PATCH 253/434] Add AbilityMixinStaminaCost proto --- proto/AbilityMixinCostStamina.proto | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 proto/AbilityMixinCostStamina.proto diff --git a/proto/AbilityMixinCostStamina.proto b/proto/AbilityMixinCostStamina.proto new file mode 100644 index 000000000..a9000d479 --- /dev/null +++ b/proto/AbilityMixinCostStamina.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message AbilityMixinCostStamina { + bool is_swim = 1; +} From fbffb8d277c77606704d034c967fdde0c3d1afb5 Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 05:00:57 -0700 Subject: [PATCH 254/434] Reset first tick on new skill --- .../game/managers/StaminaManager/StaminaManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index cde40ee57..8737d9755 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -127,6 +127,7 @@ public class StaminaManager { // Accessors public void setSkillCast(int skillId, int skillCasterId) { + lastSkillFirstTick = true; lastSkillId = skillId; lastSkillCasterId = skillCasterId; } @@ -460,10 +461,10 @@ public class StaminaManager { }}; if (talentMovementConsumptions.containsKey(skillCasting)) { if (lastSkillFirstTick) { - consumption = talentMovementConsumptions.get(skillCasting).get(1); + consumption = talentMovementConsumptions.get(skillCasting).get(0); } else { lastSkillFirstTick = false; - consumption = talentMovementConsumptions.get(skillCasting).get(0); + consumption = talentMovementConsumptions.get(skillCasting).get(1); } } // TODO: Claymore avatar charged attack From b133825dd49f5f7d92e274a3434a7ea9c5d190dd Mon Sep 17 00:00:00 2001 From: Secretboy <74841174+Secretboy-SMR@users.noreply.github.com> Date: Tue, 10 May 2022 20:33:45 +0800 Subject: [PATCH 255/434] add /language command (#780) * Fix the following issues: 1. HashMap non-thread-safe issus 2. Fix the same problem in pr621, but use a better implementation Add the following functions: 1. There is now a language cache inside getLanguage to prepare for different languages corresponding to different time zones where the accounts in the server are located * add /language command,each account has their own Locate --- .../java/emu/grasscutter/Grasscutter.java | 8 ++ .../command/commands/AccountCommand.java | 16 +-- .../command/commands/BroadcastCommand.java | 4 +- .../command/commands/ChangeSceneCommand.java | 12 +- .../command/commands/ClearCommand.java | 24 ++-- .../command/commands/CoopCommand.java | 12 +- .../command/commands/DropCommand.java | 14 +- .../command/commands/EnterDungeonCommand.java | 12 +- .../command/commands/GiveAllCommand.java | 10 +- .../command/commands/GiveArtifactCommand.java | 14 +- .../command/commands/GiveCharCommand.java | 14 +- .../command/commands/GiveCommand.java | 24 ++-- .../command/commands/GodModeCommand.java | 4 +- .../command/commands/HealCommand.java | 4 +- .../command/commands/HelpCommand.java | 26 ++-- .../command/commands/KickCommand.java | 6 +- .../command/commands/KillAllCommand.java | 10 +- .../commands/KillCharacterCommand.java | 4 +- .../command/commands/ListCommand.java | 2 +- .../command/commands/PermissionCommand.java | 18 +-- .../command/commands/PositionCommand.java | 4 +- .../command/commands/ReloadCommand.java | 4 +- .../command/commands/ResetConstCommand.java | 8 +- .../commands/ResetShopLimitCommand.java | 4 +- .../command/commands/SendMailCommand.java | 52 +++---- .../command/commands/SendMessageCommand.java | 8 +- .../commands/SetFetterLevelCommand.java | 10 +- .../command/commands/SetStatsCommand.java | 12 +- .../commands/SetWorldLevelCommand.java | 10 +- .../command/commands/SpawnCommand.java | 14 +- .../command/commands/StopCommand.java | 4 +- .../command/commands/TalentCommand.java | 34 ++--- .../command/commands/TeleportAllCommand.java | 6 +- .../command/commands/TeleportCommand.java | 12 +- .../command/commands/UnlockTowerCommand.java | 2 +- .../command/commands/WeatherCommand.java | 10 +- .../java/emu/grasscutter/game/Account.java | 16 +++ .../java/emu/grasscutter/utils/Language.java | 130 +++++++++++++++--- src/main/resources/languages/en-US.json | 5 + src/main/resources/languages/zh-CN.json | 5 + 40 files changed, 356 insertions(+), 232 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index f426d41af..8bdb3c207 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -206,6 +206,14 @@ public final class Grasscutter { return language; } + public static void setLanguage(Language language) { + Grasscutter.language = language; + } + + public static Language getLanguage(String langCode) { + return Language.getLanguage(langCode); + } + public static Logger getLogger() { return log; } diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index a16cc6480..2669ff8b9 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -17,12 +17,12 @@ public final class AccountCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (sender != null) { - CommandHandler.sendMessage(sender, translate("commands.generic.console_execute_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.console_execute_error")); return; } if (args.size() < 2) { - CommandHandler.sendMessage(null, translate("commands.account.command_usage")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.command_usage")); return; } @@ -31,7 +31,7 @@ public final class AccountCommand implements CommandHandler { switch (action) { default: - CommandHandler.sendMessage(null, translate("commands.account.command_usage")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.command_usage")); return; case "create": int uid = 0; @@ -39,20 +39,20 @@ public final class AccountCommand implements CommandHandler { try { uid = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, translate("commands.account.invalid")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.invalid")); return; } } emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithId(username, uid); if (account == null) { - CommandHandler.sendMessage(null, translate("commands.account.exists")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.exists")); return; } else { account.addPermission("*"); account.save(); // Save account to database. - CommandHandler.sendMessage(null, translate("commands.account.create", Integer.toString(account.getPlayerUid()))); + CommandHandler.sendMessage(null, translate(sender, "commands.account.create", Integer.toString(account.getPlayerUid()))); } return; case "delete": @@ -60,7 +60,7 @@ public final class AccountCommand implements CommandHandler { Account toDelete = DatabaseHelper.getAccountByName(username); if (toDelete == null) { - CommandHandler.sendMessage(null, translate("commands.account.no_account")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.no_account")); return; } @@ -73,7 +73,7 @@ public final class AccountCommand implements CommandHandler { // Finally, we do the actual deletion. DatabaseHelper.deleteAccount(toDelete); - CommandHandler.sendMessage(null, translate("commands.account.delete")); + CommandHandler.sendMessage(null, translate(sender, "commands.account.delete")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java index 95f0c7c05..86080c822 100644 --- a/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/BroadcastCommand.java @@ -15,7 +15,7 @@ public final class BroadcastCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (args.size() < 1) { - CommandHandler.sendMessage(sender, translate("commands.broadcast.command_usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.broadcast.command_usage")); return; } @@ -25,6 +25,6 @@ public final class BroadcastCommand implements CommandHandler { CommandHandler.sendMessage(p, message); } - CommandHandler.sendMessage(sender, translate("commands.broadcast.message_sent")); + CommandHandler.sendMessage(sender, translate(sender, "commands.broadcast.message_sent")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java index 59706dd96..c653acee1 100644 --- a/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ChangeSceneCommand.java @@ -14,31 +14,31 @@ public final class ChangeSceneCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() != 1) { - CommandHandler.sendMessage(sender, translate("commands.changescene.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.changescene.usage")); return; } try { int sceneId = Integer.parseInt(args.get(0)); if (sceneId == targetPlayer.getSceneId()) { - CommandHandler.sendMessage(sender, translate("commands.changescene.already_in_scene")); + CommandHandler.sendMessage(sender, translate(sender, "commands.changescene.already_in_scene")); return; } boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, targetPlayer.getPos()); if (!result) { - CommandHandler.sendMessage(sender, translate("commands.changescene.exists_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.changescene.exists_error")); return; } - CommandHandler.sendMessage(sender, translate("commands.changescene.success", Integer.toString(sceneId))); + CommandHandler.sendMessage(sender, translate(sender, "commands.changescene.success", Integer.toString(sceneId))); } catch (Exception e) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java index ab0ff0eb1..7b50ee034 100644 --- a/src/main/java/emu/grasscutter/command/commands/ClearCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ClearCommand.java @@ -21,11 +21,11 @@ public final class ClearCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() < 1) { - CommandHandler.sendMessage(sender, translate("commands.clear.command_usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.command_usage")); return; } Inventory playerInventory = targetPlayer.getInventory(); @@ -37,7 +37,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getItemType() == ItemType.ITEM_WEAPON) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, translate("commands.clear.weapons", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.weapons", targetPlayer.getNickname())); } case "art" -> { toDelete = playerInventory.getItems().values().stream() @@ -45,7 +45,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getLevel() == 1 && item.getExp() == 0) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, translate("commands.clear.artifacts", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.artifacts", targetPlayer.getNickname())); } case "mat" -> { toDelete = playerInventory.getItems().values().stream() @@ -53,7 +53,7 @@ public final class ClearCommand implements CommandHandler { .filter(item -> item.getLevel() == 1 && item.getExp() == 0) .filter(item -> !item.isLocked() && !item.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, translate("commands.clear.materials", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.materials", targetPlayer.getNickname())); } case "all" -> { toDelete = playerInventory.getItems().values().stream() @@ -61,7 +61,7 @@ public final class ClearCommand implements CommandHandler { .filter(item1 -> item1.getLevel() == 1 && item1.getExp() == 0) .filter(item1 -> !item1.isLocked() && !item1.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, translate("commands.clear.artifacts", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.artifacts", targetPlayer.getNickname())); playerInventory.removeItems(toDelete); toDelete = playerInventory.getItems().values().stream() @@ -69,7 +69,7 @@ public final class ClearCommand implements CommandHandler { .filter(item2 -> !item2.isLocked() && !item2.isEquipped()) .toList(); playerInventory.removeItems(toDelete); - CommandHandler.sendMessage(sender, translate("commands.clear.materials", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.materials", targetPlayer.getNickname())); toDelete = playerInventory.getItems().values().stream() .filter(item3 -> item3.getItemType() == ItemType.ITEM_WEAPON) @@ -77,28 +77,28 @@ public final class ClearCommand implements CommandHandler { .filter(item3 -> !item3.isLocked() && !item3.isEquipped()) .toList(); playerInventory.removeItems(toDelete); - CommandHandler.sendMessage(sender, translate("commands.clear.weapons", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.weapons", targetPlayer.getNickname())); toDelete = playerInventory.getItems().values().stream() .filter(item4 -> item4.getItemType() == ItemType.ITEM_FURNITURE) .filter(item4 -> !item4.isLocked() && !item4.isEquipped()) .toList(); playerInventory.removeItems(toDelete); - CommandHandler.sendMessage(sender, translate("commands.clear.furniture", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.furniture", targetPlayer.getNickname())); toDelete = playerInventory.getItems().values().stream() .filter(item5 -> item5.getItemType() == ItemType.ITEM_DISPLAY) .filter(item5 -> !item5.isLocked() && !item5.isEquipped()) .toList(); playerInventory.removeItems(toDelete); - CommandHandler.sendMessage(sender, translate("commands.clear.displays", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.displays", targetPlayer.getNickname())); toDelete = playerInventory.getItems().values().stream() .filter(item6 -> item6.getItemType() == ItemType.ITEM_VIRTUAL) .filter(item6 -> !item6.isLocked() && !item6.isEquipped()) .toList(); - CommandHandler.sendMessage(sender, translate("commands.clear.virtuals", targetPlayer.getNickname())); - CommandHandler.sendMessage(sender, translate("commands.clear.everything", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.virtuals", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.clear.everything", targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java index a86472978..509c554b7 100644 --- a/src/main/java/emu/grasscutter/command/commands/CoopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/CoopCommand.java @@ -15,14 +15,14 @@ public final class CoopCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } Player host = sender; switch (args.size()) { case 0: // Summon target to self - CommandHandler.sendMessage(sender, translate("commands.coop.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.coop.usage")); if (sender == null) // Console doesn't have a self to summon to return; break; @@ -31,16 +31,16 @@ public final class CoopCommand implements CommandHandler { int hostId = Integer.parseInt(args.get(0)); host = Grasscutter.getGameServer().getPlayerByUid(hostId); if (host == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.player_offline_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.player_offline_error")); return; } break; } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.execution.uid_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.uid_error")); return; } default: - CommandHandler.sendMessage(sender, translate("commands.coop.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.coop.usage")); return; } @@ -50,6 +50,6 @@ public final class CoopCommand implements CommandHandler { } host.getServer().getMultiplayerManager().applyEnterMp(targetPlayer, host.getUid()); targetPlayer.getServer().getMultiplayerManager().applyEnterMpReply(host, targetPlayer.getUid(), true); - CommandHandler.sendMessage(sender, translate("commands.coop.success", targetPlayer.getNickname(), host.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.coop.success", targetPlayer.getNickname(), host.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/DropCommand.java b/src/main/java/emu/grasscutter/command/commands/DropCommand.java index e2579a7be..eecec06e9 100644 --- a/src/main/java/emu/grasscutter/command/commands/DropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/DropCommand.java @@ -19,7 +19,7 @@ public final class DropCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(null, translate("commands.execution.need_target")); + CommandHandler.sendMessage(null, translate(sender, "commands.execution.need_target")); return; } @@ -31,25 +31,25 @@ public final class DropCommand implements CommandHandler { try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount")); return; } // Slightly cheeky here: no break, so it falls through to initialize the first argument too case 1: try { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId")); return; } break; default: - CommandHandler.sendMessage(sender, translate("commands.drop.command_usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.drop.command_usage")); return; } ItemData itemData = GameData.getItemDataMap().get(item); if (itemData == null) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId")); return; } if (itemData.isEquip()) { @@ -63,6 +63,6 @@ public final class DropCommand implements CommandHandler { EntityItem entity = new EntityItem(targetPlayer.getScene(), targetPlayer, itemData, targetPlayer.getPos().clone().addY(3f), amount); targetPlayer.getScene().addEntity(entity); } - CommandHandler.sendMessage(sender, translate("commands.drop.success", Integer.toString(amount), Integer.toString(item))); + CommandHandler.sendMessage(sender, translate(sender, "commands.drop.success", Integer.toString(amount), Integer.toString(item))); } -} \ No newline at end of file +} diff --git a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java index c4e37a93e..b44952393 100644 --- a/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EnterDungeonCommand.java @@ -14,30 +14,30 @@ public final class EnterDungeonCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(null, translate("commands.execution.need_target")); + CommandHandler.sendMessage(null, translate(sender, "commands.execution.need_target")); return; } if (args.size() < 1) { - CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.usage")); return; } try { int dungeonId = Integer.parseInt(args.get(0)); if (dungeonId == targetPlayer.getSceneId()) { - CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.in_dungeon_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.in_dungeon_error")); return; } boolean result = targetPlayer.getServer().getDungeonManager().enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId); - CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.changed", dungeonId)); + CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.changed", dungeonId)); if (!result) { - CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.not_found_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.not_found_error")); } } catch (Exception e) { - CommandHandler.sendMessage(sender, translate("commands.enter_dungeon.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.usage")); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java index 6b9104626..3a0dee111 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveAllCommand.java @@ -21,7 +21,7 @@ public final class GiveAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } int amount = 99999; @@ -33,21 +33,21 @@ public final class GiveAllCommand implements CommandHandler { try { amount = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount")); return; } break; default: // invalid - CommandHandler.sendMessage(sender, translate("commands.giveAll.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveAll.usage")); return; } this.giveAllItems(targetPlayer, amount); - CommandHandler.sendMessage(sender, translate("commands.giveAll.success", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(targetPlayer, "commands.giveAll.success", targetPlayer.getNickname())); } public void giveAllItems(Player player, int amount) { - CommandHandler.sendMessage(player, translate("commands.giveAll.started")); + CommandHandler.sendMessage(player, translate(player, "commands.giveAll.started")); for (AvatarData avatarData: GameData.getAvatarDataMap().values()) { //Exclude test avatar diff --git a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java index 25503dbd3..6f3a02a5e 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveArtifactCommand.java @@ -115,11 +115,11 @@ public final class GiveArtifactCommand implements CommandHandler { public void execute(Player sender, Player targetPlayer, List<String> args) { // Sanity checks if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() < 2) { - CommandHandler.sendMessage(sender, translate("commands.giveArtifact.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveArtifact.usage")); return; } @@ -128,13 +128,13 @@ public final class GiveArtifactCommand implements CommandHandler { try { itemId = Integer.parseInt(args.remove(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveArtifact.id_error")); return; } ItemData itemData = GameData.getItemDataMap().get(itemId); if (itemData.getItemType() != ItemType.ITEM_RELIQUARY) { - CommandHandler.sendMessage(sender, translate("commands.giveArtifact.id_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveArtifact.id_error")); return; } @@ -155,7 +155,7 @@ public final class GiveArtifactCommand implements CommandHandler { } if (mainPropId == -1) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); return; } @@ -194,7 +194,7 @@ public final class GiveArtifactCommand implements CommandHandler { appendPropIdList.addAll(Collections.nCopies(n, appendPropId)); }); } catch (Exception ignored) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); return; } @@ -206,7 +206,7 @@ public final class GiveArtifactCommand implements CommandHandler { item.getAppendPropIdList().addAll(appendPropIdList); targetPlayer.getInventory().addItem(item, ActionReason.SubfieldDrop); - CommandHandler.sendMessage(sender, translate("commands.giveArtifact.success", Integer.toString(itemId), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveArtifact.success", Integer.toString(itemId), Integer.toString(targetPlayer.getUid()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index af789f394..2917c64c4 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -18,7 +18,7 @@ public final class GiveCharCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -31,7 +31,7 @@ public final class GiveCharCommand implements CommandHandler { level = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarLevel")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.invalid.avatarLevel")); return; } // Cheeky fall-through to parse first argument too case 1: @@ -39,24 +39,24 @@ public final class GiveCharCommand implements CommandHandler { avatarId = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from avatar name using GM Handbook. - CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.invalid.avatarId")); return; } break; default: - CommandHandler.sendMessage(sender, translate("commands.giveChar.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveChar.usage")); return; } AvatarData avatarData = GameData.getAvatarDataMap().get(avatarId); if (avatarData == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.invalid.avatarId")); return; } // Check level. if (level > 90) { - CommandHandler.sendMessage(sender, translate("commands.execution.invalid.avatarLevel")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.invalid.avatarLevel")); return; } @@ -76,6 +76,6 @@ public final class GiveCharCommand implements CommandHandler { avatar.recalcStats(); targetPlayer.addAvatar(avatar); - CommandHandler.sendMessage(sender, translate("commands.giveChar.given", Integer.toString(avatarId), Integer.toString(level), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.giveChar.given", Integer.toString(avatarId), Integer.toString(level), Integer.toString(targetPlayer.getUid()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java index a60991bad..9ce353419 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java @@ -34,7 +34,7 @@ public final class GiveCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } int item; @@ -68,21 +68,21 @@ public final class GiveCommand implements CommandHandler { try { refinement = Integer.parseInt(args.get(3)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemRefinement")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemRefinement")); return; } // Fallthrough case 3: // <itemId|itemName> [amount] [level] try { lvl = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemLevel")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemLevel")); return; } // Fallthrough case 2: // <itemId|itemName> [amount] try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount")); return; } // Fallthrough case 1: // <itemId|itemName> @@ -90,28 +90,28 @@ public final class GiveCommand implements CommandHandler { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId")); return; } break; default: // *No args* - CommandHandler.sendMessage(sender, translate("commands.give.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.usage")); return; } ItemData itemData = GameData.getItemDataMap().get(item); if (itemData == null) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId")); return; } if (refinement != 0) { if (itemData.getItemType() == ItemType.ITEM_WEAPON) { if (refinement < 1 || refinement > 5) { - CommandHandler.sendMessage(sender, translate("commands.give.refinement_must_between_1_and_5")); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.refinement_must_between_1_and_5")); return; } } else { - CommandHandler.sendMessage(sender, translate("commands.give.refinement_only_applicable_weapons")); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.refinement_only_applicable_weapons")); return; } } @@ -119,11 +119,11 @@ public final class GiveCommand implements CommandHandler { this.item(targetPlayer, itemData, amount, lvl, refinement); if (!itemData.isEquip()) { - CommandHandler.sendMessage(sender, translate("commands.give.given", Integer.toString(amount), Integer.toString(item), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.given", Integer.toString(amount), Integer.toString(item), Integer.toString(targetPlayer.getUid()))); } else if (itemData.getItemType() == ItemType.ITEM_WEAPON) { - CommandHandler.sendMessage(sender, translate("commands.give.given_with_level_and_refinement", Integer.toString(item), Integer.toString(lvl), Integer.toString(refinement), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.given_with_level_and_refinement", Integer.toString(item), Integer.toString(lvl), Integer.toString(refinement), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); } else { - CommandHandler.sendMessage(sender, translate("commands.give.given_level", Integer.toString(item), Integer.toString(lvl), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.given_level", Integer.toString(item), Integer.toString(lvl), Integer.toString(amount), Integer.toString(targetPlayer.getUid()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java index 98a375838..9b9160bcd 100644 --- a/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GodModeCommand.java @@ -14,7 +14,7 @@ public final class GodModeCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -35,6 +35,6 @@ public final class GodModeCommand implements CommandHandler { } targetPlayer.setGodmode(enabled); - CommandHandler.sendMessage(sender, translate("commands.godmode.success", (enabled ? translate("commands.status.enabled") : translate("commands.status.disabled")), targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.godmode.success", (enabled ? translate(sender, "commands.status.enabled") : translate(sender, "commands.status.disabled")), targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/HealCommand.java b/src/main/java/emu/grasscutter/command/commands/HealCommand.java index 78ff14405..0eb92356f 100644 --- a/src/main/java/emu/grasscutter/command/commands/HealCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HealCommand.java @@ -17,7 +17,7 @@ public final class HealCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -32,6 +32,6 @@ public final class HealCommand implements CommandHandler { entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); } }); - CommandHandler.sendMessage(sender, translate("commands.heal.success")); + CommandHandler.sendMessage(sender, translate(sender, "commands.heal.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java index 8a222f7a6..fc94426d7 100644 --- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java @@ -32,16 +32,16 @@ public final class HelpCommand implements CommandHandler { } else { String command = args.get(0); CommandHandler handler = CommandMap.getInstance().getHandler(command); - StringBuilder builder = new StringBuilder(player == null ? "\n" + translate("commands.status.help") + " - " : translate("commands.status.help") + " - ").append(command).append(": \n"); + StringBuilder builder = new StringBuilder(player == null ? "\n" + translate(player, "commands.status.help") + " - " : translate(player, "commands.status.help") + " - ").append(command).append(": \n"); if (handler == null) { - builder.append(translate("commands.generic.command_exist_error")); + builder.append(translate(player, "commands.generic.command_exist_error")); } else { Command annotation = handler.getClass().getAnnotation(Command.class); - builder.append(" ").append(translate(annotation.description())).append("\n"); - builder.append(translate("commands.help.usage")).append(annotation.usage()); + builder.append(" ").append(translate(player, annotation.description())).append("\n"); + builder.append(translate(player, "commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(translate("commands.help.aliases")); + builder.append("\n").append(translate(player, "commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } @@ -57,13 +57,13 @@ public final class HelpCommand implements CommandHandler { void SendAllHelpMessage(Player player, List<Command> annotations) { if (player == null) { - StringBuilder builder = new StringBuilder("\n" + translate("commands.help.available_commands") + "\n"); + StringBuilder builder = new StringBuilder("\n" + translate(player, "commands.help.available_commands") + "\n"); annotations.forEach(annotation -> { builder.append(annotation.label()).append("\n"); - builder.append(" ").append(translate(annotation.description())).append("\n"); - builder.append(translate("commands.help.usage")).append(annotation.usage()); + builder.append(" ").append(translate(player, annotation.description())).append("\n"); + builder.append(translate(player, "commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(translate("commands.help.aliases")); + builder.append("\n").append(translate(player, "commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } @@ -74,13 +74,13 @@ public final class HelpCommand implements CommandHandler { CommandHandler.sendMessage(null, builder.toString()); } else { - CommandHandler.sendMessage(player, translate("commands.help.available_commands")); + CommandHandler.sendMessage(player, translate(player, "commands.help.available_commands")); annotations.forEach(annotation -> { StringBuilder builder = new StringBuilder(annotation.label()).append("\n"); - builder.append(" ").append(translate(annotation.description())).append("\n"); - builder.append(translate("commands.help.usage")).append(annotation.usage()); + builder.append(" ").append(translate(player, annotation.description())).append("\n"); + builder.append(translate(player, "commands.help.usage")).append(annotation.usage()); if (annotation.aliases().length >= 1) { - builder.append("\n").append(translate("commands.help.aliases")); + builder.append("\n").append(translate(player, "commands.help.aliases")); for (String alias : annotation.aliases()) { builder.append(alias).append(" "); } diff --git a/src/main/java/emu/grasscutter/command/commands/KickCommand.java b/src/main/java/emu/grasscutter/command/commands/KickCommand.java index 9741226e7..57837666c 100644 --- a/src/main/java/emu/grasscutter/command/commands/KickCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KickCommand.java @@ -14,16 +14,16 @@ public final class KickCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (sender != null) { - CommandHandler.sendMessage(sender, translate("commands.kick.player_kick_player", + CommandHandler.sendMessage(sender, translate(sender, "commands.kick.player_kick_player", Integer.toString(sender.getAccount().getPlayerUid()), sender.getAccount().getUsername(), Integer.toString(targetPlayer.getUid()), targetPlayer.getAccount().getUsername())); } else { - CommandHandler.sendMessage(null, translate("commands.kick.server_kick_player", Integer.toString(targetPlayer.getUid()), targetPlayer.getAccount().getUsername())); + CommandHandler.sendMessage(null, translate(sender, "commands.kick.server_kick_player", Integer.toString(targetPlayer.getUid()), 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 index 853395fe6..a803f7d3a 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -18,7 +18,7 @@ public final class KillAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -31,14 +31,14 @@ public final class KillAllCommand implements CommandHandler { scene = targetPlayer.getWorld().getSceneById(Integer.parseInt(args.get(0))); break; default: - CommandHandler.sendMessage(sender, translate("commands.kill.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.kill.usage")); return; } } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); } if (scene == null) { - CommandHandler.sendMessage(sender, translate("commands.kill.scene_not_found_in_player_world")); + CommandHandler.sendMessage(sender, translate(sender, "commands.kill.scene_not_found_in_player_world")); return; } @@ -48,6 +48,6 @@ public final class KillAllCommand implements CommandHandler { .filter(entity -> entity instanceof EntityMonster) .toList(); toKill.forEach(entity -> sceneF.killEntity(entity, 0)); - CommandHandler.sendMessage(sender, translate("commands.kill.kill_monsters_in_scene", Integer.toString(toKill.size()), Integer.toString(scene.getId()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.kill.kill_monsters_in_scene", Integer.toString(toKill.size()), Integer.toString(scene.getId()))); } } diff --git a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java index 99a57e122..b0f5c3603 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillCharacterCommand.java @@ -19,7 +19,7 @@ public final class KillCharacterCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -32,6 +32,6 @@ public final class KillCharacterCommand implements CommandHandler { targetPlayer.getScene().removeEntity(entity); entity.onDeath(0); - CommandHandler.sendMessage(sender, translate("commands.killCharacter.success", targetPlayer.getNickname())); + CommandHandler.sendMessage(sender, translate(sender, "commands.killCharacter.success", targetPlayer.getNickname())); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ListCommand.java b/src/main/java/emu/grasscutter/command/commands/ListCommand.java index 53a274e52..5c35874cb 100644 --- a/src/main/java/emu/grasscutter/command/commands/ListCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ListCommand.java @@ -22,7 +22,7 @@ public final class ListCommand implements CommandHandler { needUID = args.get(0).equals("uid"); } - CommandHandler.sendMessage(sender, translate("commands.list.success", Integer.toString(playersMap.size()))); + CommandHandler.sendMessage(sender, translate(sender, "commands.list.success", Integer.toString(playersMap.size()))); if (playersMap.size() != 0) { StringBuilder playerSet = new StringBuilder(); diff --git a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java index 4b945b3d1..fdcda5b95 100644 --- a/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PermissionCommand.java @@ -16,12 +16,12 @@ public final class PermissionCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() != 2) { - CommandHandler.sendMessage(sender, translate("commands.permission.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.permission.usage")); return; } @@ -30,26 +30,26 @@ public final class PermissionCommand implements CommandHandler { Account account = targetPlayer.getAccount(); if (account == null) { - CommandHandler.sendMessage(sender, translate("commands.permission.account_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.permission.account_error")); return; } switch (action) { default: - CommandHandler.sendMessage(sender, translate("commands.permission.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.permission.usage")); break; case "add": if (account.addPermission(permission)) { - CommandHandler.sendMessage(sender, translate("commands.permission.add")); - } else CommandHandler.sendMessage(sender, translate("commands.permission.has_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.permission.add")); + } else CommandHandler.sendMessage(sender, translate(sender, "commands.permission.has_error")); break; case "remove": if (account.removePermission(permission)) { - CommandHandler.sendMessage(sender, translate("commands.permission.remove")); - } else CommandHandler.sendMessage(sender, translate("commands.permission.not_have_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.permission.remove")); + } else CommandHandler.sendMessage(sender, translate(sender, "commands.permission.not_have_error")); 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 index b5a250af6..530d44ae0 100644 --- a/src/main/java/emu/grasscutter/command/commands/PositionCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/PositionCommand.java @@ -15,12 +15,12 @@ public final class PositionCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } Position pos = targetPlayer.getPos(); - CommandHandler.sendMessage(sender, translate("commands.position.success", + CommandHandler.sendMessage(sender, translate(sender, "commands.position.success", Float.toString(pos.getX()), Float.toString(pos.getY()), Float.toString(pos.getZ()), Integer.toString(targetPlayer.getSceneId()))); } diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 29eb93d0d..9414a89c4 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -14,7 +14,7 @@ public final class ReloadCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - CommandHandler.sendMessage(sender, translate("commands.reload.reload_start")); + CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_start")); Grasscutter.loadConfig(); Grasscutter.loadLanguage(); @@ -23,6 +23,6 @@ public final class ReloadCommand implements CommandHandler { Grasscutter.getGameServer().getShopManager().load(); Grasscutter.getDispatchServer().loadQueries(); - CommandHandler.sendMessage(sender, translate("commands.reload.reload_done")); + CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java index e8229eaf5..3c962b950 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java @@ -17,13 +17,13 @@ public final class ResetConstCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() > 0 && args.get(0).equalsIgnoreCase("all")) { targetPlayer.getAvatars().forEach(this::resetConstellation); - CommandHandler.sendMessage(sender, translate("commands.resetConst.reset_all")); + CommandHandler.sendMessage(sender, translate(sender, "commands.resetConst.reset_all")); } else { EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity(); if (entity == null) { @@ -33,7 +33,7 @@ public final class ResetConstCommand implements CommandHandler { Avatar avatar = entity.getAvatar(); this.resetConstellation(avatar); - CommandHandler.sendMessage(sender, translate("commands.resetConst.success", avatar.getAvatarData().getName())); + CommandHandler.sendMessage(sender, translate(sender, "commands.resetConst.success", avatar.getAvatarData().getName())); } } @@ -43,4 +43,4 @@ public final class ResetConstCommand implements CommandHandler { avatar.recalcStats(); avatar.save(); } -} \ No newline at end of file +} diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java index bf5e2a4a6..d2b910811 100644 --- a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -15,12 +15,12 @@ public final class ResetShopLimitCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } targetPlayer.getShopLimit().forEach(x -> x.setNextRefreshTime(0)); targetPlayer.save(); - CommandHandler.sendMessage(sender, translate("commands.status.success")); + CommandHandler.sendMessage(sender, translate(sender, "commands.status.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java index 69aafa20b..21d9e64d9 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java @@ -39,7 +39,7 @@ public final class SendMailCommand implements CommandHandler { MailBuilder mailBuilder; switch (args.get(0).toLowerCase()) { case "help" -> { - CommandHandler.sendMessage(sender, translate(this.getClass().getAnnotation(Command.class).description()) + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); + CommandHandler.sendMessage(sender, translate(sender, this.getClass().getAnnotation(Command.class).description()) + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); return; } case "all" -> mailBuilder = new MailBuilder(true, new Mail()); @@ -47,16 +47,16 @@ public final class SendMailCommand implements CommandHandler { if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) { mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail()); } else { - CommandHandler.sendMessage(sender, translate("commands.sendMail.user_not_exist", args.get(0))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.user_not_exist", args.get(0))); return; } } } mailBeingConstructed.put(senderId, mailBuilder); - CommandHandler.sendMessage(sender, translate("commands.sendMail.start_composition")); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.start_composition")); } - case 2 -> CommandHandler.sendMessage(sender, translate("commands.sendMail.templates")); - default -> CommandHandler.sendMessage(sender, translate("commands.sendMail.invalid_arguments")); + case 2 -> CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.templates")); + default -> CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.invalid_arguments")); } } else { MailBuilder mailBuilder = mailBeingConstructed.get(senderId); @@ -65,28 +65,28 @@ public final class SendMailCommand implements CommandHandler { switch (args.get(0).toLowerCase()) { case "stop" -> { mailBeingConstructed.remove(senderId); - CommandHandler.sendMessage(sender, translate("commands.sendMail.sendCancel")); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.sendCancel")); return; } case "finish" -> { if (mailBuilder.constructionStage == 3) { if (!mailBuilder.sendToAll) { Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail); - CommandHandler.sendMessage(sender, translate("commands.sendMail.send_done", Integer.toString(mailBuilder.recipient))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_done", Integer.toString(mailBuilder.recipient))); } else { for (Player player : DatabaseHelper.getAllPlayers()) { Grasscutter.getGameServer().getPlayerByUid(player.getUid(), true).sendMail(mailBuilder.mail); } - CommandHandler.sendMessage(sender, translate("commands.sendMail.send_all_done")); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_all_done")); } mailBeingConstructed.remove(senderId); } else { - CommandHandler.sendMessage(sender, translate("commands.sendMail.not_composition_end", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.not_composition_end", getConstructionArgs(mailBuilder.constructionStage, sender))); } return; } case "help" -> { - CommandHandler.sendMessage(sender, translate("commands.sendMail.please_use", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.please_use", getConstructionArgs(mailBuilder.constructionStage, sender))); return; } default -> { @@ -94,19 +94,19 @@ public final class SendMailCommand implements CommandHandler { case 0 -> { String title = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.title = title; - CommandHandler.sendMessage(sender, translate("commands.sendMail.set_title", title)); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_title", title)); mailBuilder.constructionStage++; } case 1 -> { String contents = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.content = contents; - CommandHandler.sendMessage(sender, translate("commands.sendMail.set_contents", contents)); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_contents", contents)); mailBuilder.constructionStage++; } case 2 -> { String msgSender = String.join(" ", args.subList(0, args.size())); mailBuilder.mail.mailContent.sender = msgSender; - CommandHandler.sendMessage(sender, translate("commands.sendMail.set_message_sender", msgSender)); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_message_sender", msgSender)); mailBuilder.constructionStage++; } case 3 -> { @@ -119,21 +119,21 @@ public final class SendMailCommand implements CommandHandler { try { refinement = Integer.parseInt(args.get(3)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemRefinement")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemRefinement")); return; } // Fallthrough case 3: // <itemId|itemName> [amount] [level] try { lvl = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemLevel")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemLevel")); return; } // Fallthrough case 2: // <itemId|itemName> [amount] try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount")); return; } // Fallthrough case 1: // <itemId|itemName> @@ -141,33 +141,33 @@ public final class SendMailCommand implements CommandHandler { item = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { // TODO: Parse from item name using GM Handbook. - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.itemId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId")); return; } break; default: // *No args* - CommandHandler.sendMessage(sender, translate("commands.give.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.give.usage")); return; } mailBuilder.mail.itemList.add(new Mail.MailItem(item, amount, lvl)); - CommandHandler.sendMessage(sender, translate("commands.sendMail.send", Integer.toString(amount), Integer.toString(item), Integer.toString(lvl))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send", Integer.toString(amount), Integer.toString(item), Integer.toString(lvl))); } } } } } else { - CommandHandler.sendMessage(sender, translate("commands.sendMail.invalid_arguments_please_use", getConstructionArgs(mailBuilder.constructionStage))); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.invalid_arguments_please_use", getConstructionArgs(mailBuilder.constructionStage, sender))); } } } - private String getConstructionArgs(int stage) { + private String getConstructionArgs(int stage, Player sender) { return switch(stage) { - case 0 -> translate("commands.sendMail.title"); - case 1 -> translate("commands.sendMail.message"); - case 2 -> translate("commands.sendMail.sender"); - case 3 -> translate("commands.sendMail.arguments"); - default -> translate("commands.sendMail.error", Integer.toString(stage)); + case 0 -> translate(sender, "commands.sendMail.title"); + case 1 -> translate(sender, "commands.sendMail.message"); + case 2 -> translate(sender, "commands.sendMail.sender"); + case 3 -> translate(sender, "commands.sendMail.arguments"); + default -> translate(sender, "commands.sendMail.error", Integer.toString(stage)); }; } diff --git a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java index befbf4f00..2e6feb96d 100644 --- a/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SendMessageCommand.java @@ -15,16 +15,16 @@ public final class SendMessageCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() == 0) { - CommandHandler.sendMessage(null, translate("commands.sendMessage.usage")); + CommandHandler.sendMessage(null, translate(sender, "commands.sendMessage.usage")); return; } String message = String.join(" ", args); CommandHandler.sendMessage(targetPlayer, message); - CommandHandler.sendMessage(sender, translate("commands.sendMessage.success")); + CommandHandler.sendMessage(sender, translate(sender, "commands.sendMessage.success")); } -} \ No newline at end of file +} diff --git a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java index cf356d06b..2f838fb1a 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetFetterLevelCommand.java @@ -18,19 +18,19 @@ public final class SetFetterLevelCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() != 1) { - CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.usage")); return; } try { int fetterLevel = Integer.parseInt(args.get(0)); if (fetterLevel < 0 || fetterLevel > 10) { - CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.range_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.range_error")); return; } Avatar avatar = targetPlayer.getTeamManager().getCurrentAvatarEntity().getAvatar(); @@ -42,9 +42,9 @@ public final class SetFetterLevelCommand implements CommandHandler { avatar.save(); targetPlayer.sendPacket(new PacketAvatarFetterDataNotify(avatar)); - CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.success", fetterLevel)); + CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.success", fetterLevel)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.setFetterLevel.level_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.level_error")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java index a0572d2c2..b51b09197 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetStatsCommand.java @@ -175,13 +175,13 @@ public final class SetStatsCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - String syntax = sender == null ? translate("commands.setStats.usage_console") : translate("commands.setStats.usage_ingame"); - String usage = syntax + translate("commands.setStats.help_message"); + String syntax = sender == null ? translate(sender, "commands.setStats.usage_console") : translate(sender, "commands.setStats.usage_ingame"); + String usage = syntax + translate(sender, "commands.setStats.help_message"); String statStr; String valueStr; if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -203,7 +203,7 @@ public final class SetStatsCommand implements CommandHandler { value = Float.parseFloat(valueStr); } } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.setStats.value_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setStats.value_error")); return; } @@ -217,10 +217,10 @@ public final class SetStatsCommand implements CommandHandler { valueStr = String.format("%.0f", value); } if (targetPlayer == sender) { - CommandHandler.sendMessage(sender, translate("commands.setStats.set_self", stat.name, valueStr)); + CommandHandler.sendMessage(sender, translate(sender, "commands.setStats.set_self", stat.name, valueStr)); } else { String uidStr = targetPlayer.getAccount().getId(); - CommandHandler.sendMessage(sender, translate("commands.setStats.set_self", stat.name, uidStr, valueStr)); + CommandHandler.sendMessage(sender, translate(sender, "commands.setStats.set_self", stat.name, uidStr, valueStr)); } } else { CommandHandler.sendMessage(sender, usage); diff --git a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java index aa773159e..d09c00a91 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetWorldLevelCommand.java @@ -16,19 +16,19 @@ public final class SetWorldLevelCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() < 1) { - CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setWorldLevel.usage")); return; } try { int level = Integer.parseInt(args.get(0)); if (level > 8 || level < 0) { - CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.value_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.setWorldLevel.value_error")); return; } @@ -36,9 +36,9 @@ public final class SetWorldLevelCommand implements CommandHandler { targetPlayer.getWorld().setWorldLevel(level); targetPlayer.setWorldLevel(level); - CommandHandler.sendMessage(sender, translate("commands.setWorldLevel.success", Integer.toString(level))); + CommandHandler.sendMessage(sender, translate(sender, "commands.setWorldLevel.success", Integer.toString(level))); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(null, translate("commands.setWorldLevel.invalid_world_level")); + CommandHandler.sendMessage(null, translate(sender, "commands.setWorldLevel.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 index e3193c638..24a6bc174 100644 --- a/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SpawnCommand.java @@ -28,7 +28,7 @@ public final class SpawnCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -40,23 +40,23 @@ public final class SpawnCommand implements CommandHandler { try { level = Integer.parseInt(args.get(2)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); } // Fallthrough case 2: try { amount = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.amount")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount")); } // Fallthrough case 1: try { id = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.entityId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.entityId")); } break; default: - CommandHandler.sendMessage(sender, translate("commands.spawn.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.usage")); return; } @@ -64,7 +64,7 @@ public final class SpawnCommand implements CommandHandler { GadgetData gadgetData = GameData.getGadgetDataMap().get(id); ItemData itemData = GameData.getItemDataMap().get(id); if (monsterData == null && gadgetData == null && itemData == null) { - CommandHandler.sendMessage(sender, translate("commands.generic.invalid.entityId")); + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.entityId")); return; } Scene scene = targetPlayer.getScene(); @@ -100,7 +100,7 @@ public final class SpawnCommand implements CommandHandler { scene.addEntity(entity); } - CommandHandler.sendMessage(sender, translate("commands.spawn.success", Integer.toString(amount), Integer.toString(id))); + CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.success", Integer.toString(amount), Integer.toString(id))); } private Position GetRandomPositionInCircle(Position origin, double radius){ diff --git a/src/main/java/emu/grasscutter/command/commands/StopCommand.java b/src/main/java/emu/grasscutter/command/commands/StopCommand.java index 129b27b24..d1fa0fe75 100644 --- a/src/main/java/emu/grasscutter/command/commands/StopCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/StopCommand.java @@ -14,9 +14,9 @@ public final class StopCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { - CommandHandler.sendMessage(null, translate("commands.stop.success")); + CommandHandler.sendMessage(null, translate(sender, "commands.stop.success")); for (Player p : Grasscutter.getGameServer().getPlayers().values()) { - CommandHandler.sendMessage(p, translate("commands.stop.success")); + CommandHandler.sendMessage(p, translate(sender, "commands.stop.success")); } System.exit(1000); diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java index 40ac11b50..6cb687efe 100644 --- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java @@ -19,7 +19,7 @@ public final class TalentCommand implements CommandHandler { private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) { int oldLevel = avatar.getSkillLevelMap().get(talentId); if (talentLevel < 0 || talentLevel > 15) { - CommandHandler.sendMessage(sender, translate("commands.talent.lower_16")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.lower_16")); return; } @@ -40,20 +40,20 @@ public final class TalentCommand implements CommandHandler { } else if (talentId == depot.getEnergySkill()) { successMessage = "commands.talent.set_q"; } - CommandHandler.sendMessage(sender, translate(successMessage, talentLevel)); + CommandHandler.sendMessage(sender, translate(sender, successMessage, talentLevel)); } @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (args.size() < 1){ - CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); - CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); - CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_2")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_3")); return; } @@ -62,15 +62,15 @@ public final class TalentCommand implements CommandHandler { String cmdSwitch = args.get(0); switch (cmdSwitch) { default -> { - CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); - CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); - CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_2")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_3")); return; } case "set" -> { if (args.size() < 3) { - CommandHandler.sendMessage(sender, translate("commands.talent.usage_1")); - CommandHandler.sendMessage(sender, translate("commands.talent.usage_3")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_1")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_3")); return; } try { @@ -78,13 +78,13 @@ public final class TalentCommand implements CommandHandler { int newLevel = Integer.parseInt(args.get(2)); setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.talent.invalid_skill_id")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.invalid_skill_id")); return; } } case "n", "e", "q" -> { if (args.size() < 2) { - CommandHandler.sendMessage(sender, translate("commands.talent.usage_2")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.usage_2")); return; } AvatarSkillDepotData SkillDepot = avatar.getData().getSkillDepot(); @@ -97,7 +97,7 @@ public final class TalentCommand implements CommandHandler { int newLevel = Integer.parseInt(args.get(1)); setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.talent.invalid_level")); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.invalid_level")); return; } } @@ -105,9 +105,9 @@ public final class TalentCommand implements CommandHandler { int skillIdNorAtk = avatar.getData().getSkillDepot().getSkills().get(0); int skillIdE = avatar.getData().getSkillDepot().getSkills().get(1); int skillIdQ = avatar.getData().getSkillDepot().getEnergySkill(); - CommandHandler.sendMessage(sender, translate("commands.talent.normal_attack_id", Integer.toString(skillIdNorAtk))); - CommandHandler.sendMessage(sender, translate("commands.talent.e_skill_id", Integer.toString(skillIdE))); - CommandHandler.sendMessage(sender, translate("commands.talent.q_skill_id", Integer.toString(skillIdQ))); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.normal_attack_id", Integer.toString(skillIdNorAtk))); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.e_skill_id", Integer.toString(skillIdE))); + CommandHandler.sendMessage(sender, translate(sender, "commands.talent.q_skill_id", Integer.toString(skillIdQ))); } } } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java index bfa0ac821..23f9c6d40 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java @@ -16,12 +16,12 @@ public final class TeleportAllCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } if (!targetPlayer.getWorld().isMultiplayer()) { - CommandHandler.sendMessage(sender, translate("commands.teleportAll.error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.teleportAll.error")); return; } @@ -33,6 +33,6 @@ public final class TeleportAllCommand implements CommandHandler { player.getWorld().transferPlayerToScene(player, targetPlayer.getSceneId(), pos); } - CommandHandler.sendMessage(sender, translate("commands.teleportAll.success")); + CommandHandler.sendMessage(sender, translate(sender, "commands.teleportAll.success")); } } diff --git a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java index 8a9fb9948..62827d86c 100644 --- a/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/TeleportCommand.java @@ -27,7 +27,7 @@ public final class TeleportCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -42,7 +42,7 @@ public final class TeleportCommand implements CommandHandler { try { sceneId = Integer.parseInt(args.get(3)); }catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.execution.argument_error")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); } // Fallthrough case 3: try { @@ -50,20 +50,20 @@ public final class TeleportCommand implements CommandHandler { y = parseRelative(args.get(1), y); z = parseRelative(args.get(2), z); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.teleport.invalid_position")); + CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.invalid_position")); } break; default: - CommandHandler.sendMessage(sender, translate("commands.teleport.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.usage")); return; } Position target_pos = new Position(x, y, z); boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, target_pos); if (!result) { - CommandHandler.sendMessage(sender, translate("commands.teleport.invalid_position")); + CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.invalid_position")); } else { - CommandHandler.sendMessage(sender, translate("commands.teleport.success", + CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.success", targetPlayer.getNickname(), Float.toString(x), Float.toString(y), Float.toString(z), Integer.toString(sceneId)) ); diff --git a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java index bd7b8bc1f..c2b67209c 100644 --- a/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/UnlockTowerCommand.java @@ -21,7 +21,7 @@ public class UnlockTowerCommand implements CommandHandler { unlockFloor(sender, sender.getServer().getTowerScheduleManager() .getScheduleFloors()); - CommandHandler.sendMessage(sender, translate("commands.unlocktower.success")); + CommandHandler.sendMessage(sender, translate(sender, "commands.unlocktower.success")); } public void unlockFloor(Player player, List<Integer> floors){ diff --git a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java index 0edbd8482..7ad347465 100644 --- a/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/WeatherCommand.java @@ -17,7 +17,7 @@ public final class WeatherCommand implements CommandHandler { @Override public void execute(Player sender, Player targetPlayer, List<String> args) { if (targetPlayer == null) { - CommandHandler.sendMessage(sender, translate("commands.execution.need_target")); + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); return; } @@ -28,17 +28,17 @@ public final class WeatherCommand implements CommandHandler { try { climateId = Integer.parseInt(args.get(1)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.weather.invalid_id")); + CommandHandler.sendMessage(sender, translate(sender, "commands.weather.invalid_id")); } case 1: try { weatherId = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate("commands.weather.invalid_id")); + CommandHandler.sendMessage(sender, translate(sender, "commands.weather.invalid_id")); } break; default: - CommandHandler.sendMessage(sender, translate("commands.weather.usage")); + CommandHandler.sendMessage(sender, translate(sender, "commands.weather.usage")); return; } @@ -47,6 +47,6 @@ public final class WeatherCommand implements CommandHandler { targetPlayer.getScene().setWeather(weatherId); targetPlayer.getScene().setClimate(climate); targetPlayer.getScene().broadcastPacket(new PacketSceneAreaWeatherNotify(targetPlayer)); - CommandHandler.sendMessage(sender, translate("commands.weather.success", Integer.toString(weatherId), Integer.toString(climateId))); + CommandHandler.sendMessage(sender, translate(sender, "commands.weather.success", Integer.toString(weatherId), Integer.toString(climateId))); } } diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 7e8baa291..821dea80b 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -8,6 +8,7 @@ import emu.grasscutter.utils.Utils; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import org.bson.Document; @@ -28,10 +29,12 @@ public class Account { private String token; private String sessionKey; // Session token for dispatch server private List<String> permissions; + private Locale locale; @Deprecated public Account() { this.permissions = new ArrayList<>(); + this.locale = Grasscutter.getConfig().LocaleLanguage; } public String getId() { @@ -96,6 +99,14 @@ public class Account { return this.sessionKey; } + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + /** * The collection of a player's permissions. */ @@ -166,5 +177,10 @@ public class Account { if (!document.containsKey("permissions")) { this.addPermission("*"); } + + // Set account default language as server default language + if (!document.containsKey("locale")) { + this.locale = Grasscutter.getConfig().LocaleLanguage; + } } } diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 04bd352f7..57f211020 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -3,6 +3,8 @@ package emu.grasscutter.utils; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.player.Player; import javax.annotation.Nullable; import java.io.InputStream; @@ -11,6 +13,7 @@ import java.util.Map; public final class Language { private final JsonObject languageData; + private final String languageCode; private final Map<String, String> cachedTranslations = new ConcurrentHashMap<>(); private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>(); @@ -24,8 +27,21 @@ public final class Language { return cachedLanguages.get(langCode); } - var languageInst = new Language(langCode + ".json", Utils.getLanguageCode(Grasscutter.getConfig().DefaultLanguage) + ".json"); - cachedLanguages.put(langCode, languageInst); + var fallbackLanguageCode = Utils.getLanguageCode(Grasscutter.getConfig().DefaultLanguage); + var descripter = getLanguageFileStreamDescripter(langCode, fallbackLanguageCode); + var actualLanguageCode = descripter.getLanguageCode(); + + Language languageInst = null; + + if (descripter.getLanguageFile() != null) { + languageInst = new Language(descripter); + cachedLanguages.put(actualLanguageCode, languageInst); + } + else { + languageInst = cachedLanguages.get(actualLanguageCode); + cachedLanguages.put(langCode, languageInst); + } + return languageInst; } @@ -47,34 +63,90 @@ public final class Language { } /** - * Reads a file and creates a language instance. - * @param fileName The name of the language file. - * @param fallback The name of the fallback language file. + * Returns the translated value from the key while substituting arguments. + * @param player Target player + * @param key The key of the translated value to return. + * @param args The arguments to substitute. + * @return A translated value with arguments substituted. */ - private Language(String fileName, String fallback) { - @Nullable JsonObject languageData = null; + public static String translate(Player player, String key, Object... args) { + if (player == null) { + return translate(key, args); + } - InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); - if (file == null) { // Provided fallback language. - file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); - Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); - } - if(file == null) { // Fallback the fallback language. - file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); - Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); - } - if(file == null) - throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files."); + var langCode = Utils.getLanguageCode(player.getAccount().getLocale()); + String translated = Grasscutter.getLanguage(langCode).get(key); try { - languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); + return translated.formatted(args); } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception); + Grasscutter.getLogger().error("Failed to format string: " + key, exception); + return translated; + } + } + + /** + * get language code + */ + public String getLanguageCode() { + return languageCode; + } + + /** + * Reads a file and creates a language instance. + */ + private Language(InternalLanguageFileStreamDescripter descripter) { + @Nullable JsonObject languageData = null; + languageCode = descripter.getLanguageCode(); + + try { + languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(descripter.getLanguageFile()), JsonObject.class); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to load language file: " + descripter.getLanguageCode(), exception); } this.languageData = languageData; } + /** + * create a InternalLanguageFileStreamDescripter + * @param languageCode The name of the language code. + * @param fallbackLanguageCode The name of the fallback language code. + */ + private static InternalLanguageFileStreamDescripter getLanguageFileStreamDescripter(String languageCode, String fallbackLanguageCode) { + var fileName = languageCode + ".json"; + var fallback = fallbackLanguageCode + ".json"; + + String actualLanguageCode = languageCode; + if (cachedLanguages.containsKey(actualLanguageCode)) { + return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + } + InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); + + if (file == null) { // Provided fallback language. + actualLanguageCode = fallbackLanguageCode; + if (cachedLanguages.containsKey(actualLanguageCode)) { + return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + } + file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); + Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); + } + + if(file == null) { // Fallback the fallback language. + actualLanguageCode = "en-US"; + if (cachedLanguages.containsKey(actualLanguageCode)) { + return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + } + file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); + Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); + } + + if(file == null) + throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files."); + + return new InternalLanguageFileStreamDescripter(actualLanguageCode, file); + } + /** * Returns the value (as a string) from a nested key. * @param key The key to look for. @@ -107,4 +179,22 @@ public final class Language { this.cachedTranslations.put(key, result); return result; } + + private static class InternalLanguageFileStreamDescripter { + private String languageCode; + private InputStream languageFile; + + public InternalLanguageFileStreamDescripter(String languageCode, InputStream languageFile) { + this.languageCode = languageCode; + this.languageFile = languageFile; + } + + public String getLanguageCode() { + return languageCode; + } + + public InputStream getLanguageFile() { + return languageFile; + } + } } diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 9e7271ea3..893f490af 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -188,6 +188,11 @@ "success": "Killed %s's current character.", "description": "Kills the players current character" }, + "language": { + "current_language": "current language is %s", + "language_changed": "language changed to %s", + "description": "display or change current language" + }, "list": { "success": "There are %s player(s) online:", "description": "List online players" diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index f6a2de0ea..a326d4960 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -188,6 +188,11 @@ "success": "已杀死 %s 当前角色。", "description": "杀死当前角色" }, + "language": { + "current_language": "当前语言是: %s", + "language_changed": "语言切换至: %s", + "description": "显示或切换当前语言" + }, "list": { "success": "目前在线人数:%s", "description": "查看所有玩家" From 0a95b7fe2e294de7cd407134f3ffdfaf014eee36 Mon Sep 17 00:00:00 2001 From: Secretboy <74841174+Secretboy-SMR@users.noreply.github.com> Date: Tue, 10 May 2022 21:35:37 +0800 Subject: [PATCH 256/434] fixed pr780, uploaded LanguageCommand.java (#782) * Fix the following issues: 1. HashMap non-thread-safe issus 2. Fix the same problem in pr621, but use a better implementation Add the following functions: 1. There is now a language cache inside getLanguage to prepare for different languages corresponding to different time zones where the accounts in the server are located * add /language command,each account has their own Locate * I forgot to git add...sorry,,this pr is to fix pr780, uploaded LanguageCommand.java --- .../command/commands/LanguageCommand.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/emu/grasscutter/command/commands/LanguageCommand.java diff --git a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java new file mode 100644 index 000000000..e16455697 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java @@ -0,0 +1,49 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.utils.Utils; + +import java.util.List; +import java.util.Locale; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "language", usage = "language [language code]", description = "commands.language.description", aliases = {"lang"}) +public final class LanguageCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List<String> args) { + if (args.isEmpty()) { + String curLangCode = null; + if (sender != null) { + curLangCode = Utils.getLanguageCode(sender.getAccount().getLocale()); + } + else { + curLangCode = Grasscutter.getLanguage().getLanguageCode(); + } + CommandHandler.sendMessage(sender, translate(sender, "commands.language.current_language", curLangCode)); + return; + } + + String langCode = args.get(0); + String actualLangCode = null; + if (sender != null) { + var locale = Locale.forLanguageTag(langCode); + actualLangCode = Utils.getLanguageCode(locale); + sender.getAccount().setLocale(locale); + return; + } + else { + var languageInst = Grasscutter.getLanguage(langCode); + actualLangCode = languageInst.getLanguageCode(); + Grasscutter.setLanguage(languageInst); + } + CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_changed", actualLangCode)); + + } +} From 4efdc767aea3a8413a596d7da2123f414dbe1582 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Tue, 10 May 2022 21:18:15 +0800 Subject: [PATCH 257/434] Improve text --- src/main/resources/languages/zh-CN.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index a326d4960..8d5b35137 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -14,7 +14,7 @@ "general_error": "[Dispatch] 加载 keystore 文件时发生错误!", "password_error": "[Dispatch] 加载 keystore 失败。正在尝试使用 keystore 默认密码...", "no_keystore_error": "[Dispatch] 未找到 SSL 证书!已降级到 HTTP 模式", - "default_password": "[Dispatch] keystore 默认密码加载成功。请考虑将 config.json 的默认密码设置为 123456" + "default_password": "[Dispatch] 成功加载 keystore 默认密码。请考虑将 config.json 的默认密码设置为 123456" }, "no_commands_error": "此命令不适用于 Dispatch-only 模式", "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", @@ -181,21 +181,21 @@ "usage": "用法:killall [玩家UID] [场景ID]", "scene_not_found_in_player_world": "未在玩家世界中找到此场景", "kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。", - "description": "杀死所有怪物" + "description": "杀死所有怪物。" }, "killCharacter": { "usage": "用法:/killcharacter [玩家ID]", "success": "已杀死 %s 当前角色。", - "description": "杀死当前角色" + "description": "杀死当前角色。" }, "language": { "current_language": "当前语言是: %s", "language_changed": "语言切换至: %s", - "description": "显示或切换当前语言" + "description": "显示或切换当前语言。" }, "list": { "success": "目前在线人数:%s", - "description": "查看所有玩家" + "description": "查看所有玩家。" }, "permission": { "usage": "用法:permission <add|remove> <用户名> <权限>", @@ -250,7 +250,7 @@ "sendMessage": { "usage": "用法:sendmessage <玩家> <消息>", "success": "消息已发送。", - "description": "向指定玩家发送消息" + "description": "向指定玩家发送消息。" }, "setFetterLevel": { "usage": "用法:setfetterlevel <好感度等级>", @@ -262,7 +262,7 @@ "setStats": { "usage_console": "用法:setstats|stats @<UID> <属性> <数值>", "usage_ingame": "用法:setstats|stats [@UID] <属性> <数值>", - "help_message": "\n\t可更改的属性列表:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n\t(续) 元素增伤:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)\n\t(续) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", + "help_message": "\n可更改的属性列表:hp (生命值)| maxhp (最大生命值) | def(防御力) | atk (攻击力)| em (元素精通) | er (元素充能效率) | crate(暴击率) | cdmg (暴击伤害)| cdr (冷却缩减) | heal(治疗加成)| heali (受治疗加成)| shield (护盾强效)| defi (无视防御)\n(续) 元素增伤:epyro (火) | ecryo (冰) | ehydro (水) | egeo (岩) | edendro (草) | eelectro (雷) | ephys (物理)\n(续) 元素抗性:respyro (火) | rescryo (冰) | reshydro (水) | resgeo (岩) | resdendro (草) | reselectro (雷) | resphys (物理)\n", "value_error": "无效的属性值。", "uid_error": "无效的UID。", "player_error": "玩家不存在或已离线。", @@ -285,7 +285,7 @@ }, "stop": { "success": "正在关闭服务器...", - "description": "停止服务器" + "description": "停止服务器。" }, "talent": { "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", @@ -324,7 +324,7 @@ "usage": "用法:weather <天气ID> [气候ID]", "success": "已更改天气为 %s,气候为 %s。", "invalid_id": "无效的天气ID。", - "description": "更改天气" + "description": "更改天气。" }, "drop": { "command_usage": "用法:drop <物品ID|物品名称> [数量]", @@ -342,10 +342,10 @@ }, "unlocktower": { "success": "解锁完成。", - "description": "解锁深境螺旋的所有层" + "description": "解锁深境螺旋的所有层。" }, "resetshop": { - "description": "重置商店时间" + "description": "重置商店刷新时间。" } } } From 2969abc20e33f715303845c31f1419a566006fb0 Mon Sep 17 00:00:00 2001 From: Secretboy-SMR <secretboy.smr@icloud.com> Date: Tue, 10 May 2022 22:57:52 +0800 Subject: [PATCH 258/434] Fix language switching prompt and save --- .../emu/grasscutter/command/commands/LanguageCommand.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java index e16455697..5966c6167 100644 --- a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java @@ -35,8 +35,9 @@ public final class LanguageCommand implements CommandHandler { if (sender != null) { var locale = Locale.forLanguageTag(langCode); actualLangCode = Utils.getLanguageCode(locale); - sender.getAccount().setLocale(locale); - return; + var account = sender.getAccount(); + account.setLocale(locale); + account.save(); } else { var languageInst = Grasscutter.getLanguage(langCode); From a25eb631c4cc298cb0905998996e35443a38b9fd Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Tue, 10 May 2022 20:00:47 -0400 Subject: [PATCH 259/434] my poor, poor, language system (Formatting refactor) --- .../java/emu/grasscutter/utils/Language.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 57f211020..7c3426384 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -3,7 +3,6 @@ package emu.grasscutter.utils; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.Account; import emu.grasscutter.game.player.Player; import javax.annotation.Nullable; @@ -12,10 +11,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.Map; public final class Language { + private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>(); + private final JsonObject languageData; private final String languageCode; private final Map<String, String> cachedTranslations = new ConcurrentHashMap<>(); - private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>(); /** * Creates a language instance from a code. @@ -28,16 +28,14 @@ public final class Language { } var fallbackLanguageCode = Utils.getLanguageCode(Grasscutter.getConfig().DefaultLanguage); - var descripter = getLanguageFileStreamDescripter(langCode, fallbackLanguageCode); - var actualLanguageCode = descripter.getLanguageCode(); + var description = getLanguageFileStreamDescripter(langCode, fallbackLanguageCode); + var actualLanguageCode = description.getLanguageCode(); - Language languageInst = null; - - if (descripter.getLanguageFile() != null) { - languageInst = new Language(descripter); + Language languageInst; + if (description.getLanguageFile() != null) { + languageInst = new Language(description); cachedLanguages.put(actualLanguageCode, languageInst); - } - else { + } else { languageInst = cachedLanguages.get(actualLanguageCode); cachedLanguages.put(langCode, languageInst); } @@ -95,39 +93,41 @@ public final class Language { /** * Reads a file and creates a language instance. */ - private Language(InternalLanguageFileStreamDescripter descripter) { + private Language(LanguageStreamDescription description) { @Nullable JsonObject languageData = null; - languageCode = descripter.getLanguageCode(); + languageCode = description.getLanguageCode(); try { - languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(descripter.getLanguageFile()), JsonObject.class); + languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class); } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to load language file: " + descripter.getLanguageCode(), exception); + Grasscutter.getLogger().warn("Failed to load language file: " + description.getLanguageCode(), exception); } this.languageData = languageData; } /** - * create a InternalLanguageFileStreamDescripter + * create a LanguageStreamDescription * @param languageCode The name of the language code. * @param fallbackLanguageCode The name of the fallback language code. */ - private static InternalLanguageFileStreamDescripter getLanguageFileStreamDescripter(String languageCode, String fallbackLanguageCode) { + private static LanguageStreamDescription getLanguageFileStreamDescripter(String languageCode, String fallbackLanguageCode) { var fileName = languageCode + ".json"; var fallback = fallbackLanguageCode + ".json"; String actualLanguageCode = languageCode; if (cachedLanguages.containsKey(actualLanguageCode)) { - return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + return new LanguageStreamDescription(actualLanguageCode, null); } + InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); if (file == null) { // Provided fallback language. actualLanguageCode = fallbackLanguageCode; if (cachedLanguages.containsKey(actualLanguageCode)) { - return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + return new LanguageStreamDescription(actualLanguageCode, null); } + file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); } @@ -135,8 +135,9 @@ public final class Language { if(file == null) { // Fallback the fallback language. actualLanguageCode = "en-US"; if (cachedLanguages.containsKey(actualLanguageCode)) { - return new InternalLanguageFileStreamDescripter(actualLanguageCode, null); + return new LanguageStreamDescription(actualLanguageCode, null); } + file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); } @@ -144,7 +145,7 @@ public final class Language { if(file == null) throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files."); - return new InternalLanguageFileStreamDescripter(actualLanguageCode, file); + return new LanguageStreamDescription(actualLanguageCode, file); } /** @@ -180,11 +181,11 @@ public final class Language { this.cachedTranslations.put(key, result); return result; } - private static class InternalLanguageFileStreamDescripter { - private String languageCode; - private InputStream languageFile; + private static class LanguageStreamDescription { + private final String languageCode; + private final InputStream languageFile; - public InternalLanguageFileStreamDescripter(String languageCode, InputStream languageFile) { + public LanguageStreamDescription(String languageCode, InputStream languageFile) { this.languageCode = languageCode; this.languageFile = languageFile; } From ef9d63f1dd811423fb911c0ca17e92a502f3489a Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Tue, 10 May 2022 15:18:01 -0700 Subject: [PATCH 260/434] Claymore charged attack stamina cost --- .../StaminaManager/StaminaManager.java | 249 +++++++++++++----- 1 file changed, 188 insertions(+), 61 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 8737d9755..0795a152d 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -1,5 +1,6 @@ package emu.grasscutter.game.managers.StaminaManager; +import ch.qos.logback.classic.Logger; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.GameEntity; @@ -55,7 +56,7 @@ public class StaminaManager { MotionState.MOTION_LADDER_TO_STANDBY, // NOT OBSERVED MotionState.MOTION_STANDBY_MOVE, // sustained, recover MotionState.MOTION_STANDBY // sustained, recover - ))); + ))); put("SWIM", new HashSet<>(List.of( MotionState.MOTION_SWIM_IDLE, // sustained MotionState.MOTION_SWIM_DASH, // immediate and sustained @@ -104,6 +105,7 @@ public class StaminaManager { ))); }}; + private final Logger logger = Grasscutter.getLogger(); public final static int GlobalMaximumStamina = 24000; private Position currentCoordinates = new Position(0, 0, 0); private Position previousCoordinates = new Position(0, 0, 0); @@ -118,6 +120,73 @@ public class StaminaManager { private int lastSkillId = 0; private int lastSkillCasterId = 0; private boolean lastSkillFirstTick = true; + public static final HashSet<Integer> TalentMovements = new HashSet<>(List.of( + 10013, // Kamisato Ayaka + 10413 // Mona + )); + + // TODO: Get from somewhere else, instead of hard-coded here? + public static final HashSet<Integer> ClaymoreSkills = new HashSet<>(List.of( + 10160, // Diluc, /=2 + 10201, // Razor + 10241, // Beidou + 10341, // Noelle + 10401, // Chongyun + 10441, // Xinyan + 10511, // Eula + 10531, // Sayu + 10571 // Arataki Itto, = 0 + )); + public static final HashSet<Integer> CatalystSkills = new HashSet<>(List.of( + 10060, // Lisa + 10070, // Barbara + 10271, // Ningguang + 10291, // Klee + 10411, // Mona + 10431, // Sucrose + 10481, // Yanfei + 10541, // Sangonomoiya Kokomi + 10581 // Yae Miko + )); + public static final HashSet<Integer> PolearmSkills = new HashSet<>(List.of( + 10231, // Xiangling + 10261, // Xiao + 10301, // Zhongli + 10451, // Rosaria + 10461, // Hu Tao + 10501, // Thoma + 10521, // Raiden Shogun + 10631, // Shenhe + 10641 // Yunjin + )); + public static final HashSet<Integer> SwordSkills = new HashSet<>(List.of( + 10024, // Kamisato Ayaka + 10031, // Jean + 10073, // Kaeya + 10321, // Bennett + 10337, // Tartaglia, melee stance (10332 switch to melee, 10336 switch to ranged stance) + 10351, // Qiqi + 10381, // Xingqiu + 10386, // Albedo + 10421, // Keqing, =-2500 + 10471, // Kaedehara Kazuha + 10661, // Kamisato Ayato + 100553, // Lumine + 100540 // Aether + )); + public static final HashSet<Integer> BowSkills = new HashSet<>(List.of( + 10041, 10043, // Amber + 10221, 10223,// Venti + 10311, 10315, // Fischl + 10331, 10335, // Tartaglia, ranged stance + 10371, // Ganyu + 10391, 10394, // Diona + 10491, // Yoimiya + 10551, 10554, // Gorou + 10561, 10564, // Kojou Sara + 10621, // Aloy + 99998, 99999 // Yelan // TODO: get real values + )); public StaminaManager(Player player) { @@ -168,7 +237,7 @@ public class StaminaManager { float diffX = currentCoordinates.getX() - previousCoordinates.getX(); float diffY = currentCoordinates.getY() - previousCoordinates.getY(); float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); - Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + + logger.trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; } @@ -182,14 +251,14 @@ public class StaminaManager { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption); if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) { - Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" + + logger.debug("[StaminaManager] Stamina update relative(" + consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" + consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); return currentStamina; } } int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); - Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + + logger.trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," + consumption.amount + ")"); int newStamina = currentStamina + consumption.amount; @@ -207,7 +276,7 @@ public class StaminaManager { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina); if (overriddenNewStamina != newStamina) { - Grasscutter.getLogger().debug("[StaminaManager] Stamina update absolute(" + + logger.debug("[StaminaManager] Stamina update absolute(" + reason + ", " + newStamina + ") overridden to absolute(" + reason + ", " + newStamina + ") by: " + listener.getKey()); return currentStamina; @@ -254,7 +323,7 @@ public class StaminaManager { if (!player.isPaused() && sustainedStaminaHandlerTimer == null) { sustainedStaminaHandlerTimer = new Timer(); sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); - Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started"); + logger.debug("[MovementManager] SustainedStaminaHandlerTimer started"); } } @@ -262,7 +331,7 @@ public class StaminaManager { if (sustainedStaminaHandlerTimer != null) { sustainedStaminaHandlerTimer.cancel(); sustainedStaminaHandlerTimer = null; - Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); + logger.debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); } } @@ -276,12 +345,26 @@ public class StaminaManager { return; } setSkillCast(skillId, casterId); + // Handle immediate stamina cost + if (ClaymoreSkills.contains(skillId)) { + // Exclude claymore as their stamina cost starts when MixinStaminaCost gets in + return; + } + // TODO: Differentiate normal attacks from charged attacks and exclude + // TODO: Temporary: Exclude non-claymore attacks for now + if (BowSkills.contains(skillId) + || SwordSkills.contains(skillId) + || PolearmSkills.contains(skillId) + || CatalystSkills.contains(skillId) + ) { + return; + } handleImmediateStamina(session, skillId); } public void handleMixinCostStamina(boolean isSwim) { // Talent moving and claymore avatar charged attack duration - // Grasscutter.getLogger().trace("abilityMixinCostStamina: isSwim: " + isSwim); + // logger.trace("abilityMixinCostStamina: isSwim: " + isSwim); if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) { handleImmediateStamina(cachedSession, lastSkillId); } @@ -299,7 +382,7 @@ public class StaminaManager { return; } currentState = motionState; - // Grasscutter.getLogger().trace("" + currentState); + // logger.trace("" + currentState); Vector posVector = motionInfo.getPos(); Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { @@ -337,8 +420,6 @@ public class StaminaManager { } private void handleImmediateStamina(GameSession session, int skillId) { - // Non-claymore avatar attacks - // TODO: differentiate charged vs normal attack Consumption consumption = getFightConsumption(skillId); updateStaminaRelative(session, consumption); } @@ -349,7 +430,7 @@ public class StaminaManager { int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); if (moving || (currentStamina < maxStamina)) { - Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " + + logger.trace("Player moving: " + moving + ", stamina full: " + (currentStamina >= maxStamina) + ", recalculate stamina"); Consumption consumption; @@ -396,7 +477,7 @@ public class StaminaManager { // For others recover after 2 seconds (10 ticks) - as official server does. staminaRecoverDelay++; consumption.amount = 0; - Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); + logger.trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); } } updateStaminaRelative(cachedSession, consumption); @@ -414,7 +495,7 @@ public class StaminaManager { private void handleDrowning() { int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); if (stamina < 10) { - Grasscutter.getLogger().trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + + logger.trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState); if (currentState != MotionState.MOTION_SWIM_IDLE) { killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); @@ -427,52 +508,32 @@ public class StaminaManager { // Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina private Consumption getFightConsumption(int skillCasting) { - /* TODO: - Instead of handling here, consider call StaminaManager.updateStamina****() with a Consumption object with - type=FIGHT and a modified amount when handling attacks for more accurate attack start/end time and - other info. Handling it here could be very complicated. - Charged attack - Default: - Polearm: (-2500) - Claymore: (-4000 per second, -800 each tick) - Catalyst: (-5000) - Talent: - Ningguang: When Ningguang is in possession of Star Jades, her Charged Attack does not consume Stamina. (Catalyst * 0) - Klee: When Jumpy Dumpty and Normal Attacks deal DMG, Klee has a 50% chance to obtain an Explosive Spark. - This Explosive Spark is consumed by the next Charged Attack, which costs no Stamina. (Catalyst * 0) - Constellations: - Hu Tao: While in a Paramita Papilio state activated by Guide to Afterlife, Hu Tao's Charge Attacks do not consume Stamina. (Polearm * 0) - Character Specific: - Keqing: (-2500) - Diluc: (Claymore * 0.5) - Talent Moving: (Those are skills too) - Ayaka: (-1000 initial) (-1500 per second) When the Cryo application at the end of Kamisato Art: Senho hits an opponent (+1000) - Mona: (-1000 initial) (-1500 per second) - */ - - // TODO: Currently only handling Ayaka and Mona's talent moving initial costs. - Consumption consumption = new Consumption(); - // Talent moving - HashMap<Integer, List<Consumption>> talentMovementConsumptions = new HashMap<>() {{ - // List[0] = initial cost, [1] = sustained cost. Sustained costs are divided by 3 per second as MixinStaminaCost is triggered at 3Hz. - put(10013, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Kamisato Ayaka - put(10413, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Mona - }}; - if (talentMovementConsumptions.containsKey(skillCasting)) { - if (lastSkillFirstTick) { - consumption = talentMovementConsumptions.get(skillCasting).get(0); - } else { - lastSkillFirstTick = false; - consumption = talentMovementConsumptions.get(skillCasting).get(1); - } + if (TalentMovements.contains(skillCasting)) { + // TODO: recover 1000 if kamisato hits an enemy at the end of dashing + return getTalentMovingSustainedCost(skillCasting); } - // TODO: Claymore avatar charged attack - // HashMap<Integer, Integer> fightConsumptions = new HashMap<>(); - - // TODO: Non-claymore avatar charged attack - - return consumption; + // Bow avatar charged attack + if (BowSkills.contains(skillCasting)) { + return getBowSustainedCost(skillCasting); + } + // Claymore avatar charged attack + if (ClaymoreSkills.contains(skillCasting)) { + return getClaymoreSustainedCost(skillCasting); + } + // Catalyst avatar charged attack + if (CatalystSkills.contains(skillCasting)) { + return getCatalystSustainedCost(skillCasting); + } + // Polearm avatar charged attack + if (PolearmSkills.contains(skillCasting)) { + return getPolearmSustainedCost(skillCasting); + } + // Sword avatar charged attack + if (SwordSkills.contains(skillCasting)) { + return getSwordSustainedCost(skillCasting); + } + return new Consumption(); } private Consumption getClimbConsumption() { @@ -550,13 +611,17 @@ public class StaminaManager { if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) { return new Consumption(ConsumptionType.POWERED_SKIFF); } - Consumption consumption = new Consumption(ConsumptionType.SKIFF); // No known reduction for skiffing. - return consumption; + return new Consumption(ConsumptionType.SKIFF); } private Consumption getOtherConsumptions() { - // TODO: Add logic + if (currentState == MotionState.MOTION_NOTIFY) { + if (BowSkills.contains(lastSkillId)) { + return new Consumption(ConsumptionType.FIGHT, 500); + } + } + // TODO: Add other logic return new Consumption(); } @@ -584,4 +649,66 @@ public class StaminaManager { float reduction = 1; return reduction; } + + private Consumption getTalentMovingSustainedCost(int skillId) { + if (lastSkillFirstTick) { + lastSkillFirstTick = false; + return new Consumption(ConsumptionType.TALENT_DASH, -1000); + } else { + return new Consumption(ConsumptionType.TALENT_DASH, -500); + } + } + + private Consumption getBowSustainedCost(int skillId) { + // Note that bow skills actually recovers stamina + // Character specific handling + // switch (skillId) { + // // No known bow skills cost stamina + // } + return new Consumption(ConsumptionType.FIGHT, +500); + } + + private Consumption getCatalystSustainedCost(int skillId) { + Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000); + // Character specific handling + switch (skillId) { + // TODO: Yanfei + } + return consumption; + } + + private Consumption getClaymoreSustainedCost(int skillId) { + Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333 + // Character specific handling + switch (skillId) { + case 10571: // Arataki Itto, does not consume stamina at all. + consumption.amount = 0; + break; + case 10160: // Diluc, with talent "Relentless" stamina cost is decreased by 50% + // TODO: How to get talent status? + consumption.amount /= 2; + break; + } + return consumption; + } + + private Consumption getPolearmSustainedCost(int skillId) { + Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500); + // Character specific handling + switch (skillId) { + // TODO: + } + return consumption; + } + + private Consumption getSwordSustainedCost(int skillId) { + Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000); + // Character specific handling + switch (skillId) { + case 10421: // Keqing, -2500 + consumption.amount = -2500; + break; + } + return consumption; + } } From 5ad58a4566f3f57d447f5c9d0d5353b8350c8eb1 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Tue, 10 May 2022 22:49:25 -0400 Subject: [PATCH 261/434] Create new config class --- .../java/emu/grasscutter/Configuration.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/main/java/emu/grasscutter/Configuration.java diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java new file mode 100644 index 000000000..9437b1655 --- /dev/null +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -0,0 +1,166 @@ +package emu.grasscutter; + +import emu.grasscutter.Grasscutter.*; +import emu.grasscutter.game.mail.Mail.*; + +import java.util.Locale; + +/** + * A data container for the server's configuration. + */ +public final class Configuration { + public Structure folderStructure; + public Database databaseInfo; + public Language language; + public Server server; + + /* Option containers. */ + + public static class Database { + public String connectionUri = "mongodb://localhost:27017"; + public String collection = "grasscutter"; + } + + public static class Structure { + public String resources = "./resources/"; + public String data = "./data/"; + public String packets = "./packets/"; + public String keys = "./keys/"; + public String scripts = "./resources/scripts/"; + public String plugins = "./plugins/"; + + // UNUSED (potentially added later?) + // public String dumps = "./dumps/"; + } + + public static class Server { + public ServerDebugMode debugLevel = ServerDebugMode.NONE; + public ServerRunMode runMode = ServerRunMode.HYBRID; + + public Dispatch dispatch = new Dispatch(); + public Game game = new Game(); + } + + public static class Language { + public Locale language = Locale.getDefault(); + public Locale fallback = Locale.US; + } + + /* Server options. */ + + public static class Dispatch { + public String bindAddress = "0.0.0.0"; + /* This is the address used in URLs. */ + public String accessAddress = "127.0.0.1"; + + public int bindPort = 443; + /* This is the port used in URLs. */ + public int accessPort = 443; + + public Encryption encryption = new Encryption(); + public Policies policies = new Policies(); + public Region[] regions = {}; + } + + public static class Game { + public String bindAddress = "0.0.0.0"; + /* This is the address used in the default region. */ + public String accessAddress = "127.0.0.1"; + + public int bindPort = 443; + /* This is the port used in the default region. */ + public int accessPort = 443; + + public GameOptions gameOptions = new GameOptions(); + public JoinOptions joinOptions = new JoinOptions(); + public ConsoleAccount serverAccount = new ConsoleAccount(); + } + + /* Data containers. */ + + public static class Encryption { + public boolean useEncryption = true; + /* Should 'https' be appended to URLs? */ + public boolean useInRouting = true; + public String keystore = "./keystore.p12"; + public String keystorePassword = "123456"; + } + + public static class Policies { + public CORS cors = new CORS(); + + public static class CORS { + public boolean enabled = false; + public String[] allowedOrigins = new String[]{"*"}; + } + } + + public static class GameOptions { + public InventoryLimits inventoryLimits = new InventoryLimits(); + public AvatarLimits avatarLimits = new AvatarLimits(); + public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. + + public boolean watchGachaConfiguration = false; + public boolean enableShopItems = true; + public Rates rates = new Rates(); + + public static class InventoryLimits { + public int weapons = 2000; + public int relics = 2000; + public int materials = 2000; + public int furniture = 2000; + public int all = 30000; + } + + public static class AvatarLimits { + public int singlePlayerTeam = 4; + public int multiplayerTeam = 4; + } + + public static class Rates { + public float adventureExp = 1.0f; + public float mora = 1.0f; + public float leyLines = 1.0f; + } + } + + public static class JoinOptions { + public int[] welcomeEmotes = {2007, 1002, 4010}; + public String welcomeMessage = "Welcome to a Grasscutter server."; + + public static class Mail { + public String title = "Welcome to Grasscutter!"; + public String content = """ + Hi there!\r + First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r + \r + Check out our:\r + <type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/> + """; + public String sender = "Lawnmower"; + public MailItem[] items = { + new MailItem(13509, 1, 1), + new MailItem(201, 99999, 1) + }; + } + } + + public static class ConsoleAccount { + public int avatarId = 10000007; + public int nameCardId = 210001; + public int adventureRank = 1; + public int worldLevel = 0; + + public String nickName = "Server"; + public String signature = "Welcome to Grasscutter!"; + } + + /* Objects. */ + + public static class Region { + public String Name = "os_usa"; + public String Title = "Grasscutter"; + public String Ip = "127.0.0.1"; + public int Port = 22102; + } +} \ No newline at end of file From 6894ab8aae57f929bedc9953956754335ad3032b Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 00:30:07 -0400 Subject: [PATCH 262/434] Convert to the far superior config system --- .../java/emu/grasscutter/Configuration.java | 137 +++++++++++++++++- .../java/emu/grasscutter/Grasscutter.java | 99 ++++++++----- .../command/commands/ReloadCommand.java | 2 +- .../emu/grasscutter/data/ResourceLoader.java | 30 ++-- .../grasscutter/database/DatabaseManager.java | 19 +-- .../java/emu/grasscutter/game/Account.java | 6 +- .../grasscutter/game/drop/DropManager.java | 8 +- .../game/expedition/ExpeditionManager.java | 4 +- .../grasscutter/game/gacha/GachaBanner.java | 15 +- .../grasscutter/game/gacha/GachaManager.java | 34 ++--- .../grasscutter/game/inventory/Inventory.java | 42 +++--- .../StaminaManager/StaminaManager.java | 5 +- .../emu/grasscutter/game/player/Player.java | 8 +- .../emu/grasscutter/game/player/TeamInfo.java | 10 +- .../grasscutter/game/player/TeamManager.java | 25 ++-- .../grasscutter/game/shop/ShopManager.java | 10 +- .../game/tower/TowerScheduleManager.java | 19 ++- .../emu/grasscutter/game/world/World.java | 15 -- .../java/emu/grasscutter/plugin/Plugin.java | 4 +- .../emu/grasscutter/plugin/PluginManager.java | 5 +- .../scripts/SceneScriptManager.java | 12 +- .../server/dispatch/AnnouncementHandler.java | 21 +-- .../dispatch/DispatchHttpJsonHandler.java | 5 +- .../server/dispatch/DispatchServer.java | 110 ++++++-------- .../DefaultAuthenticationHandler.java | 5 +- .../dispatch/http/GachaRecordHandler.java | 9 +- .../grasscutter/server/game/GameServer.java | 8 +- .../server/game/GameServerPacketHandler.java | 4 +- .../grasscutter/server/game/GameSession.java | 10 +- .../recv/HandlerSetPlayerBornDataReq.java | 11 +- .../send/PacketGetPlayerFriendListRsp.java | 18 ++- .../packet/send/PacketPlayerLoginRsp.java | 14 +- .../packet/send/PacketPlayerStoreNotify.java | 6 +- .../packet/send/PacketPullRecentChatRsp.java | 19 ++- .../send/PacketStoreWeightLimitNotify.java | 13 +- .../java/emu/grasscutter/tools/Tools.java | 88 +++++------ .../java/emu/grasscutter/utils/Crypto.java | 12 +- .../java/emu/grasscutter/utils/Language.java | 8 +- .../java/emu/grasscutter/utils/Utils.java | 7 +- 39 files changed, 504 insertions(+), 373 deletions(-) diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 9437b1655..369160f42 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -3,17 +3,141 @@ package emu.grasscutter; import emu.grasscutter.Grasscutter.*; import emu.grasscutter.game.mail.Mail.*; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Locale; +import static emu.grasscutter.Grasscutter.config; + /** * A data container for the server's configuration. + * + * Use `import static emu.grasscutter.Configuration.*;` + * to import all configuration constants. */ public final class Configuration { + private static int version() { + return 1; + } + + /** + * Attempts to update the server's existing configuration to the latest configuration. + */ + public static void updateConfig() { + var existing = config.version; + var latest = version(); + + if(existing == latest) + return; + + // Create a new configuration instance. + Configuration updated = new Configuration(); + // Update all configuration fields. + Field[] fields = Configuration.class.getDeclaredFields(); + Arrays.stream(fields).forEach(field -> { + try { + field.set(updated, field.get(config)); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to update a configuration field.", exception); + } + }); + + try { // Save configuration & reload. + Grasscutter.saveConfig(updated); + Grasscutter.reloadConfig(); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to inject the updated configuration.", exception); + } + } + + /* + * Constants + */ + + // 'c' is short for 'config' and makes code look 'cleaner'. + public static final Configuration c = config; + + public static final Locale LANGUAGE = config.language.language; + public static final Locale FALLBACK_LANGUAGE = config.language.fallback; + public static final String DATA_FOLDER = config.folderStructure.data; + public static final String RESOURCES_FOLDER = config.folderStructure.resources; + public static final String KEYS_FOLDER = config.folderStructure.keys; + public static final String PLUGINS_FOLDER = config.folderStructure.plugins; + public static final String SCRIPTS_FOLDER = config.folderStructure.scripts; + public static final String PACKETS_FOLDER = config.folderStructure.packets; + + public static final Server SERVER = config.server; + public static final Database DATABASE = config.databaseInfo; + public static final Account ACCOUNT = config.account; + + public static final Dispatch DISPATCH_INFO = config.server.dispatch; + public static final Game GAME_INFO = config.server.game; + + public static final Encryption DISPATCH_ENCRYPTION = config.server.dispatch.encryption; + public static final Policies DISPATCH_POLICIES = config.server.dispatch.policies; + + public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions; + public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; + + /* + * Utilities + */ + + public static String DATA(String path) { + return DATA_FOLDER + "/" + path; + } + + public static String RESOURCE(String path) { + return RESOURCES_FOLDER + "/" + path; + } + + public static String SCRIPT(String path) { + return SCRIPTS_FOLDER + "/" + path; + } + + /** + * Fallback method. + * @param left Attempt to use. + * @param right Use if left is undefined. + * @return Left or right. + */ + public static <T> T lr(T left, T right) { + return left == null ? right : left; + } + + /** + * {@link Configuration#lr(Object, Object)} for {@link String}s. + * @param left Attempt to use. + * @param right Use if left is empty. + * @return Left or right. + */ + public static String lr(String left, String right) { + return left.isEmpty() ? right : left; + } + + /** + * {@link Configuration#lr(Object, Object)} for {@link Integer}s. + * @param left Attempt to use. + * @param right Use if left is 0. + * @return Left or right. + */ + public static int lr(int left, int right) { + return left == 0 ? right : left; + } + + /* + * Configuration data. + */ + public Structure folderStructure; public Database databaseInfo; public Language language; + public Account account; public Server server; + // DO NOT. TOUCH. THE VERSION NUMBER. + public int version = version(); + /* Option containers. */ public static class Database { @@ -45,6 +169,11 @@ public final class Configuration { public Locale language = Locale.getDefault(); public Locale fallback = Locale.US; } + + public static class Account { + public boolean autoCreate = false; + public String[] defaultPermissions = {}; + } /* Server options. */ @@ -60,6 +189,8 @@ public final class Configuration { public Encryption encryption = new Encryption(); public Policies policies = new Policies(); public Region[] regions = {}; + + public String defaultName = "Grasscutter"; } public static class Game { @@ -100,10 +231,13 @@ public final class Configuration { public AvatarLimits avatarLimits = new AvatarLimits(); public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. - public boolean watchGachaConfiguration = false; + public boolean watchGachaConfig = false; public boolean enableShopItems = true; + public boolean staminaUsage = true; public Rates rates = new Rates(); + public Database databaseInfo = new Database(); + public static class InventoryLimits { public int weapons = 2000; public int relics = 2000; @@ -127,6 +261,7 @@ public final class Configuration { public static class JoinOptions { public int[] welcomeEmotes = {2007, 1002, 4010}; public String welcomeMessage = "Welcome to a Grasscutter server."; + public Mail welcomeMail = new Mail(); public static class Mail { public String title = "Welcome to Grasscutter!"; diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 8bdb3c207..07cc1f240 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -1,9 +1,8 @@ package emu.grasscutter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOError; +import java.io.*; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Calendar; import emu.grasscutter.command.CommandMap; @@ -32,13 +31,15 @@ import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; +import javax.annotation.Nullable; + import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public final class Grasscutter { private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class); private static LineReader consoleLineReader = null; - - private static Config config; + private static Language language; private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); @@ -51,13 +52,14 @@ public final class Grasscutter { private static PluginManager pluginManager; public static final Reflections reflector = new Reflections("emu.grasscutter"); + public static final Configuration config; static { // Declare logback configuration. System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); // Load server configuration. - Grasscutter.loadConfig(); + config = Grasscutter.loadConfig(); // Load translation files. Grasscutter.loadLanguage(); @@ -66,9 +68,9 @@ public final class Grasscutter { Utils.startupCheck(); } - public static void main(String[] args) throws Exception { - Crypto.loadKeys(); // Load keys from buffers. - + public static void main(String[] args) throws Exception { + Crypto.loadKeys(); // Load keys from buffers. + // Parse arguments. boolean exitEarly = false; for (String arg : args) { @@ -77,25 +79,25 @@ public final class Grasscutter { Tools.createGmHandbook(); exitEarly = true; } case "-gachamap" -> { - Tools.createGachaMapping(Grasscutter.getConfig().DATA_FOLDER + "/gacha_mappings.js"); exitEarly = true; + Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; } } } // Exit early if argument sets it. if(exitEarly) System.exit(0); - + // Initialize server. Grasscutter.getLogger().info(translate("messages.status.starting")); - + // Load all resources. Grasscutter.updateDayOfWeek(); ResourceLoader.loadAll(); ScriptLoader.init(); - + // Initialize database. DatabaseManager.initialize(); - + // Create server instances. dispatchServer = new DispatchServer(); gameServer = new GameServer(); @@ -103,31 +105,32 @@ public final class Grasscutter { new ServerHook(gameServer, dispatchServer); // Create plugin manager instance. pluginManager = new PluginManager(); - + // Start servers. - if (getConfig().RunMode == ServerRunMode.HYBRID) { + var runMode = SERVER.runMode; + if (runMode == ServerRunMode.HYBRID) { dispatchServer.start(); gameServer.start(); - } else if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { + } else if (runMode == ServerRunMode.DISPATCH_ONLY) { dispatchServer.start(); - } else if (getConfig().RunMode == ServerRunMode.GAME_ONLY) { + } else if (runMode == ServerRunMode.GAME_ONLY) { gameServer.start(); } else { - getLogger().error(translate("messages.status.run_mode_error", getConfig().RunMode)); + getLogger().error(translate("messages.status.run_mode_error", runMode)); getLogger().error(translate("messages.status.run_mode_help")); getLogger().error(translate("messages.status.shutdown")); System.exit(1); } - + // Enable all plugins. pluginManager.enablePlugins(); - + // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); - + // Open console. startConsole(); - } + } /** * Server shutdown event. @@ -137,32 +140,60 @@ public final class Grasscutter { pluginManager.disablePlugins(); } - public static void loadConfig() { + /** + * Attempts to load the configuration from a file. + * @return The config from the file, or a new instance. + */ + public static Configuration loadConfig() { try (FileReader file = new FileReader(configFile)) { - config = gson.fromJson(file, Config.class); - saveConfig(); + return gson.fromJson(file, Configuration.class); } catch (Exception e) { - Grasscutter.config = new Config(); - saveConfig(); + Grasscutter.saveConfig(null); + return new Configuration(); } } + /** + * Attempts to reload the configuration from the file. + * Uses reflection to **replace** the fields in the config. + */ + public static void reloadConfig() { + Configuration fileConfig = Grasscutter.loadConfig(); + + Field[] fields = Configuration.class.getDeclaredFields(); + Arrays.stream(fields).forEach(field -> { + try { + field.set(config, field.get(fileConfig)); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to update a configuration field.", exception); + } + }); + } + public static void loadLanguage() { - var locale = config.LocaleLanguage; + var locale = config.language.language; language = Language.getLanguage(Utils.getLanguageCode(locale)); } - public static void saveConfig() { + /** + * Saves the provided server configuration. + * @param config The configuration to save, or null for a new one. + */ + public static void saveConfig(@Nullable Configuration config) { + if(config == null) config = new Configuration(); + try (FileWriter file = new FileWriter(configFile)) { file.write(gson.toJson(config)); + } catch (IOException ignored) { + Grasscutter.getLogger().error("Unable to write to config file."); } catch (Exception e) { - Grasscutter.getLogger().error("Unable to save config file."); + Grasscutter.getLogger().error("Unable to save config file.", e); } } public static void startConsole() { // Console should not start in dispatch only mode. - if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { + if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { getLogger().info(translate("messages.dispatch.no_commands_error")); return; } @@ -198,7 +229,7 @@ public final class Grasscutter { } } - public static Config getConfig() { + public static Configuration getConfig() { return config; } diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 9414a89c4..40b8994ec 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -16,7 +16,7 @@ public final class ReloadCommand implements CommandHandler { public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_start")); - Grasscutter.loadConfig(); + Grasscutter.reloadConfig(); Grasscutter.loadLanguage(); Grasscutter.getGameServer().getGachaManager().load(); Grasscutter.getGameServer().getDropManager().load(); diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index c2708bd63..844bbec5e 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -25,10 +25,11 @@ import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; import emu.grasscutter.data.custom.ScenePointEntry; -import emu.grasscutter.game.world.SpawnDataEntry; -import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; +import emu.grasscutter.game.world.SpawnDataEntry.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import static emu.grasscutter.Configuration.*; + public class ResourceLoader { public static List<Class<?>> getResourceDefClasses() { @@ -127,7 +128,7 @@ public class ResourceLoader { @SuppressWarnings({"rawtypes", "unchecked"}) protected static void loadFromResource(Class<?> c, String fileName, Int2ObjectMap map) throws Exception { - FileReader fileReader = new FileReader(Grasscutter.getConfig().RESOURCE_FOLDER + "ExcelBinOutput/" + fileName); + FileReader fileReader = new FileReader(RESOURCE("ExcelBinOutput/" + fileName)); Gson gson = Grasscutter.getGsonFactory(); List list = gson.fromJson(fileReader, List.class); @@ -141,7 +142,7 @@ 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"); + File folder = new File(RESOURCE("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!"); @@ -150,8 +151,7 @@ public class ResourceLoader { List<ScenePointEntry> scenePointList = new ArrayList<>(); for (File file : Objects.requireNonNull(folder.listFiles())) { - ScenePointConfig config = null; - Integer sceneId = null; + ScenePointConfig config; Integer sceneId; Matcher matcher = pattern.matcher(file.getName()); if (matcher.find()) { @@ -190,7 +190,7 @@ public class ResourceLoader { private static void loadAbilityEmbryos() { // Read from cached file if exists - File embryoCache = new File(Grasscutter.getConfig().DATA_FOLDER + "AbilityEmbryos.json"); + File embryoCache = new File(DATA("AbilityEmbryos.json")); List<AbilityEmbryoEntry> embryoList = null; if (embryoCache.exists()) { @@ -205,7 +205,7 @@ public class ResourceLoader { Pattern pattern = Pattern.compile("(?<=ConfigAvatar_)(.*?)(?=.json)"); embryoList = new LinkedList<>(); - File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Avatar/")); + File folder = new File(Utils.toFilePath(RESOURCE("BinOutput/Avatar/"))); File[] files = folder.listFiles(); if(files == null) { Grasscutter.getLogger().error("Error loading ability embryos: no files found in " + folder.getAbsolutePath()); @@ -252,7 +252,7 @@ public class ResourceLoader { private static void loadAbilityModifiers() { // Load from BinOutput - File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Ability/Temp/AvatarAbilities/")); + File folder = new File(Utils.toFilePath(RESOURCE("BinOutput/Ability/Temp/AvatarAbilities/"))); File[] files = folder.listFiles(); if (files == null) { Grasscutter.getLogger().error("Error loading ability modifiers: no files found in " + folder.getAbsolutePath()); @@ -260,7 +260,7 @@ public class ResourceLoader { } for (File file : files) { - List<AbilityConfigData> abilityConfigList = null; + List<AbilityConfigData> abilityConfigList; try (FileReader fileReader = new FileReader(file)) { abilityConfigList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityConfigData.class).getType()); @@ -315,7 +315,7 @@ public class ResourceLoader { private static void loadSpawnData() { // Read from cached file if exists - File spawnDataEntries = new File(Grasscutter.getConfig().DATA_FOLDER + "Spawns.json"); + File spawnDataEntries = new File(DATA("Spawns.json")); List<SpawnGroupEntry> spawnEntryList = null; if (spawnDataEntries.exists()) { @@ -333,16 +333,14 @@ public class ResourceLoader { } for (SpawnGroupEntry entry : spawnEntryList) { - entry.getSpawns().stream().forEach(s -> { - s.setGroup(entry); - }); + entry.getSpawns().forEach(s -> s.setGroup(entry)); GameDepot.getSpawnListById(entry.getSceneId()).insert(entry, entry.getPos().getX(), entry.getPos().getZ()); } } private static void loadOpenConfig() { // Read from cached file if exists - File openConfigCache = new File(Grasscutter.getConfig().DATA_FOLDER + "OpenConfig.json"); + File openConfigCache = new File(DATA("OpenConfig.json")); List<OpenConfigEntry> list = null; if (openConfigCache.exists()) { @@ -357,7 +355,7 @@ public class ResourceLoader { String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"}; for (String name : folderNames) { - File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + name)); + File folder = new File(Utils.toFilePath(RESOURCE(name))); File[] files = folder.listFiles(); if(files == null) { Grasscutter.getLogger().error("Error loading open config: no files found in " + folder.getAbsolutePath()); return; diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 90ff17238..37bda042b 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -1,6 +1,5 @@ package emu.grasscutter.database; -import com.mongodb.MongoClientURI; import com.mongodb.MongoCommandException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -21,11 +20,9 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; +import static emu.grasscutter.Configuration.*; + public final class DatabaseManager { - - private static MongoClient mongoClient; - private static MongoClient dispatchMongoClient; - private static Datastore datastore; private static Datastore dispatchDatastore; @@ -44,7 +41,7 @@ public final class DatabaseManager { // 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 == ServerRunMode.GAME_ONLY) { + if(SERVER.runMode == ServerRunMode.GAME_ONLY) { return dispatchDatastore; } else { return datastore; @@ -53,13 +50,13 @@ public final class DatabaseManager { public static void initialize() { // Initialize - MongoClient mongoClient = MongoClients.create(Grasscutter.getConfig().DatabaseUrl); + MongoClient mongoClient = MongoClients.create(DATABASE.connectionUri); // Set mapper options. MapperOptions mapperOptions = MapperOptions.builder() .storeEmpties(true).storeNulls(false).build(); // Create data store. - datastore = Morphia.createDatastore(mongoClient, Grasscutter.getConfig().DatabaseCollection, mapperOptions); + datastore = Morphia.createDatastore(mongoClient, DATABASE.collection, mapperOptions); // Map classes. datastore.getMapper().map(mappedClasses); @@ -80,9 +77,9 @@ public final class DatabaseManager { } } - if(Grasscutter.getConfig().RunMode == ServerRunMode.GAME_ONLY) { - dispatchMongoClient = MongoClients.create(Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseUrl); - dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, Grasscutter.getConfig().getGameServerOptions().DispatchServerDatabaseCollection); + if(SERVER.runMode == ServerRunMode.GAME_ONLY) { + MongoClient dispatchMongoClient = MongoClients.create(GAME_OPTIONS.databaseInfo.connectionUri); + dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, GAME_OPTIONS.databaseInfo.collection); // Ensure indexes for dispatch server try { diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 821dea80b..6c3daf61a 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -12,7 +12,7 @@ import java.util.Locale; import org.bson.Document; -import com.mongodb.DBObject; +import static emu.grasscutter.Configuration.*; @Entity(value = "accounts", useDiscriminator = false) public class Account { @@ -34,7 +34,7 @@ public class Account { @Deprecated public Account() { this.permissions = new ArrayList<>(); - this.locale = Grasscutter.getConfig().LocaleLanguage; + this.locale = LANGUAGE; } public String getId() { @@ -180,7 +180,7 @@ public class Account { // Set account default language as server default language if (!document.containsKey("locale")) { - this.locale = Grasscutter.getConfig().LocaleLanguage; + this.locale = LANGUAGE; } } } diff --git a/src/main/java/emu/grasscutter/game/drop/DropManager.java b/src/main/java/emu/grasscutter/game/drop/DropManager.java index e304d37b5..218624d1a 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropManager.java +++ b/src/main/java/emu/grasscutter/game/drop/DropManager.java @@ -21,6 +21,8 @@ import java.io.FileReader; import java.util.Collection; import java.util.List; +import static emu.grasscutter.Configuration.*; + public class DropManager { public GameServer getGameServer() { return gameServer; @@ -41,7 +43,7 @@ public class DropManager { } public synchronized void load() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Drop.json")) { + try (FileReader fileReader = new FileReader(DATA("Drop.json"))) { getDropData().clear(); List<DropInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType()); if(banners.size() > 0) { @@ -69,9 +71,7 @@ public class DropManager { } else { // target is null if items will be added are shared. no one could pick it up because of the combination(give + shared) // so it will be sent to all players' inventories directly. - dropScene.getPlayers().forEach(x -> { - x.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true); - }); + dropScene.getPlayers().forEach(x -> x.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true)); } } } diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java index 5d1b652e1..1b75d7306 100644 --- a/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java +++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionManager.java @@ -10,6 +10,8 @@ import java.io.FileReader; import java.util.Collection; import java.util.List; +import static emu.grasscutter.Configuration.*; + public class ExpeditionManager { public GameServer getGameServer() { return gameServer; @@ -28,7 +30,7 @@ public class ExpeditionManager { } public synchronized void load() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "ExpeditionReward.json")) { + try (FileReader fileReader = new FileReader(DATA("ExpeditionReward.json"))) { getExpeditionRewardDataList().clear(); List<ExpeditionRewardInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType()); if(banners.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index b48cb0898..dce433fcf 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -1,9 +1,10 @@ package emu.grasscutter.game.gacha; -import emu.grasscutter.Grasscutter; import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo; import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo; +import static emu.grasscutter.Configuration.*; + public class GachaBanner { private int gachaType; private int scheduleId; @@ -95,15 +96,11 @@ public class GachaBanner { public GachaInfo toProto() { return toProto(""); } + public GachaInfo toProto(String sessionKey) { - String record = "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" - + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? - Grasscutter.getConfig().getDispatchOptions().Ip : - Grasscutter.getConfig().getDispatchOptions().PublicIp) - + ":" - + Integer.toString(Grasscutter.getConfig().getDispatchOptions().PublicPort == 0 ? - Grasscutter.getConfig().getDispatchOptions().Port : - Grasscutter.getConfig().getDispatchOptions().PublicPort) + String record = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" + + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); GachaInfo.Builder info = GachaInfo.newBuilder() diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index ca7640e17..03edca09a 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -34,20 +34,22 @@ import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import org.greenrobot.eventbus.Subscribe; +import static emu.grasscutter.Configuration.*; + public class GachaManager { private final GameServer server; private final Int2ObjectMap<GachaBanner> 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}; - private int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405}; - private int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304}; + private final int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041}; + private final int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}; + private final 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}; + private final int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405}; + private final int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304}; - private static int starglitterId = 221; - private static int stardustId = 222; + private static final int starglitterId = 221; + private static final int stardustId = 222; public GachaManager(GameServer server) { this.server = server; @@ -73,7 +75,7 @@ public class GachaManager { } public synchronized void load() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Banners.json")) { + try (FileReader fileReader = new FileReader(DATA("Banners.json"))) { getGachaBanners().clear(); List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType()); if(banners.size() > 0) { @@ -242,15 +244,9 @@ public class GachaManager { } else { // Is weapon switch (itemData.getRankLevel()) { - case 5: - addStarglitter = 10; - break; - case 4: - addStarglitter = 2; - break; - case 3: - addStardust = 15; - break; + case 5 -> addStarglitter = 10; + case 4 -> addStarglitter = 2; + case 3 -> addStardust = 15; } } @@ -290,7 +286,7 @@ public class GachaManager { if(this.watchService == null) { try { this.watchService = FileSystems.getDefault().newWatchService(); - Path path = new File(Grasscutter.getConfig().DATA_FOLDER).toPath(); + Path path = new File(DATA_FOLDER).toPath(); path.register(watchService, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH); } catch (Exception e) { Grasscutter.getLogger().error("Unable to load the Gacha Manager Watch Service. If ServerOptions.watchGacha is true it will not auto-reload"); @@ -303,7 +299,7 @@ public class GachaManager { @Subscribe public synchronized void watchBannerJson(GameServerTickEvent tickEvent) { - if(Grasscutter.getConfig().getGameServerOptions().WatchGacha) { + if(GAME_OPTIONS.watchGachaConfig) { try { WatchKey watchKey = watchService.take(); diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index c4158ee6f..4a217ba54 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -6,7 +6,6 @@ import java.util.LinkedList; import java.util.List; import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.AvatarCostumeData; import emu.grasscutter.data.def.AvatarData; @@ -15,7 +14,6 @@ import emu.grasscutter.data.def.ItemData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; @@ -28,6 +26,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import static emu.grasscutter.Configuration.*; + public class Inventory implements Iterable<GameItem> { private final Player player; @@ -39,10 +39,10 @@ public class Inventory implements Iterable<GameItem> { this.store = new Long2ObjectOpenHashMap<>(); this.inventoryTypes = new Int2ObjectOpenHashMap<>(); - 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)); + this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(INVENTORY_LIMITS.weapons)); + this.createInventoryTab(ItemType.ITEM_RELIQUARY, new EquipInventoryTab(INVENTORY_LIMITS.relics)); + this.createInventoryTab(ItemType.ITEM_MATERIAL, new MaterialInventoryTab(INVENTORY_LIMITS.materials)); + this.createInventoryTab(ItemType.ITEM_FURNITURE, new MaterialInventoryTab(INVENTORY_LIMITS.furniture)); } public Player getPlayer() { @@ -242,24 +242,18 @@ public class Inventory implements Iterable<GameItem> { private void addVirtualItem(int itemId, int count) { switch (itemId) { - case 101: // Character exp - getPlayer().getServer().getInventoryManager().upgradeAvatar(player, getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), count); - break; - case 102: // Adventure exp - getPlayer().addExpDirectly(count); - break; - case 105: // Companionship exp - getPlayer().getServer().getInventoryManager().upgradeAvatarFetterLevel(player, getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), count); - break; - case 201: // Primogem - getPlayer().setPrimogems(player.getPrimogems() + count); - break; - case 202: // Mora - getPlayer().setMora(player.getMora() + count); - break; - case 203: // Genesis Crystals - getPlayer().setCrystals(player.getCrystals() + count); - break; + case 101 -> // Character exp + getPlayer().getServer().getInventoryManager().upgradeAvatar(player, getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), count); + case 102 -> // Adventure exp + getPlayer().addExpDirectly(count); + case 105 -> // Companionship exp + getPlayer().getServer().getInventoryManager().upgradeAvatarFetterLevel(player, getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), count); + case 201 -> // Primogem + getPlayer().setPrimogems(player.getPrimogems() + count); + case 202 -> // Mora + getPlayer().setMora(player.getMora() + count); + case 203 -> // Genesis Crystals + getPlayer().setCrystals(player.getCrystals() + count); } } diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 8737d9755..86006e109 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -20,6 +20,8 @@ import org.jetbrains.annotations.NotNull; import java.lang.Math; import java.util.*; +import static emu.grasscutter.Configuration.*; + public class StaminaManager { // TODO: Skiff state detection? @@ -224,9 +226,10 @@ public class StaminaManager { // Returns new stamina and sends PlayerPropNotify public int setStamina(GameSession session, String reason, int newStamina) { - if (!Grasscutter.getConfig().OpenStamina) { + if (!GAME_OPTIONS.staminaUsage) { newStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); } + // set stamina player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 477f974ea..fd5343be8 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -60,6 +60,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.util.*; import java.util.concurrent.LinkedBlockingQueue; +import static emu.grasscutter.Configuration.*; + @Entity(value = "players", useDiscriminator = false) public class Player { @@ -353,7 +355,7 @@ public class Player { } private float getExpModifier() { - return Grasscutter.getConfig().getGameServerOptions().getGameRates().ADVENTURE_EXP_RATE; + return GAME_OPTIONS.rates.adventureExp; } // Affected by exp rate @@ -1218,7 +1220,7 @@ public class Player { } else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001 // TODO: implement sanity check } else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002 - if (!(value >= 0 && value <= getSotSManager().GlobalMaximumSpringVolume)) { return false; } + if (!(value >= 0 && value <= SotSManager.GlobalMaximumSpringVolume)) { return false; } } else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003 int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME); if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; } @@ -1235,7 +1237,7 @@ public class Player { } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 if (!(0 <= value && value <= 1)) { return false; } } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 - if (!(value >= 0 && value <= getStaminaManager().GlobalMaximumStamina)) { return false; } + if (!(value >= 0 && value <= StaminaManager.GlobalMaximumStamina)) { return false; } } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } diff --git a/src/main/java/emu/grasscutter/game/player/TeamInfo.java b/src/main/java/emu/grasscutter/game/player/TeamInfo.java index 5794a7913..7d1232e50 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamInfo.java +++ b/src/main/java/emu/grasscutter/game/player/TeamInfo.java @@ -4,10 +4,10 @@ import java.util.ArrayList; import java.util.List; import dev.morphia.annotations.Entity; -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.game.avatar.Avatar; +import static emu.grasscutter.Configuration.*; + @Entity public class TeamInfo { private String name; @@ -15,7 +15,7 @@ public class TeamInfo { public TeamInfo() { this.name = ""; - this.avatars = new ArrayList<>(Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); + this.avatars = new ArrayList<>(GAME_OPTIONS.avatarLimits.singlePlayerTeam); } public TeamInfo(List<Integer> avatars) { @@ -44,7 +44,7 @@ public class TeamInfo { } public boolean addAvatar(Avatar avatar) { - if (size() >= Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam || contains(avatar)) { + if (size() >= GAME_OPTIONS.avatarLimits.singlePlayerTeam || contains(avatar)) { return false; } @@ -64,7 +64,7 @@ public class TeamInfo { } public void copyFrom(TeamInfo team) { - copyFrom(team, Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); + copyFrom(team, GAME_OPTIONS.avatarLimits.singlePlayerTeam); } public void copyFrom(TeamInfo team, int maxTeamSize) { diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 891d0a215..71961645f 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -5,7 +5,6 @@ import java.util.*; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Transient; import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.data.def.AvatarSkillDepotData; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.entity.EntityAvatar; @@ -40,6 +39,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; +import static emu.grasscutter.Configuration.*; + @Entity public class TeamManager { @Transient private Player player; @@ -174,13 +175,14 @@ public class TeamManager { public int getMaxTeamSize() { if (getPlayer().isInMultiplayer()) { - int max = Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeamMultiplayer; + int max = GAME_OPTIONS.avatarLimits.multiplayerTeam; 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().getGameServerOptions().MaxAvatarsInTeam; + + return GAME_OPTIONS.avatarLimits.singlePlayerTeam; } // Methods @@ -236,7 +238,7 @@ public class TeamManager { // Add back entities into team for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { int avatarId = this.getCurrentTeamInfo().getAvatars().get(i); - EntityAvatar entity = null; + EntityAvatar entity; if (existingAvatars.containsKey(avatarId)) { entity = existingAvatars.get(avatarId); @@ -303,8 +305,8 @@ public class TeamManager { // Set team data LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); - for (int i = 0; i < list.size(); i++) { - Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); + for (Long aLong : list) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong); if (avatar == null || newTeam.contains(avatar)) { // Should never happen return; @@ -339,8 +341,8 @@ public class TeamManager { // Set team data LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); - for (int i = 0; i < list.size(); i++) { - Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); + for (Long aLong : list) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong); if (avatar == null || newTeam.contains(avatar)) { // Should never happen return; @@ -359,7 +361,7 @@ public class TeamManager { } public void setupTemporaryTeam(List<List<Long>> guidList) { - var team = guidList.stream().map(list -> { + this.temporaryTeam = guidList.stream().map(list -> { // Sanity checks if (list.size() == 0 || list.size() > getMaxTeamSize()) { return null; @@ -367,8 +369,8 @@ public class TeamManager { // Set team data LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); - for (int i = 0; i < list.size(); i++) { - Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); + for (Long aLong : list) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong); if (avatar == null || newTeam.contains(avatar)) { // Should never happen return null; @@ -384,7 +386,6 @@ public class TeamManager { .filter(Objects::nonNull) .map(TeamInfo::new) .toList(); - this.temporaryTeam = team; } public void useTemporaryTeam(int index) { diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index 2c5d014f5..a27011012 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -16,6 +16,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import static emu.grasscutter.Configuration.*; + public class ShopManager { private final GameServer server; @@ -56,7 +58,7 @@ public class ShopManager { } private void loadShop() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Shop.json")) { + try (FileReader fileReader = new FileReader(DATA("Shop.json"))) { getShopData().clear(); List<ShopTable> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopTable.class).getType()); if(banners.size() > 0) { @@ -84,7 +86,7 @@ public class ShopManager { Grasscutter.getLogger().error("Unable to load shop data. Shop data size is 0."); } - if (Grasscutter.getConfig().getGameServerOptions().EnableOfficialShop) { + if (GAME_OPTIONS.enableShopItems) { GameData.getShopGoodsDataEntries().forEach((k, v) -> { if (!getShopData().containsKey(k.intValue())) getShopData().put(k.intValue(), new ArrayList<>()); @@ -100,7 +102,7 @@ public class ShopManager { } private void loadShopChest() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "ShopChest.json")) { + try (FileReader fileReader = new FileReader(DATA("ShopChest.json"))) { getShopChestData().clear(); List<ShopChestTable> shopChestTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestTable.class).getType()); if (shopChestTableList.size() > 0) { @@ -115,7 +117,7 @@ public class ShopManager { } private void loadShopChestBatchUse() { - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "ShopChestBatchUse.json")) { + try (FileReader fileReader = new FileReader(DATA("ShopChestBatchUse.json"))) { getShopChestBatchUseData().clear(); List<ShopChestBatchUseTable> shopChestBatchUseTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestBatchUseTable.class).getType()); if (shopChestBatchUseTableList.size() > 0) { diff --git a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java index 952acd806..ae756f009 100644 --- a/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java +++ b/src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java @@ -8,6 +8,8 @@ import emu.grasscutter.server.game.GameServer; import java.io.FileReader; import java.util.List; +import static emu.grasscutter.Configuration.*; + public class TowerScheduleManager { private final GameServer gameServer; @@ -23,9 +25,8 @@ public class TowerScheduleManager { private TowerScheduleConfig towerScheduleConfig; public synchronized void load(){ - try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "TowerSchedule.json")) { + try (FileReader fileReader = new FileReader(DATA("TowerSchedule.json"))) { towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class); - } catch (Exception e) { Grasscutter.getLogger().error("Unable to load tower schedule config.", e); } @@ -40,6 +41,7 @@ public class TowerScheduleManager { if(data == null){ Grasscutter.getLogger().error("Could not get current tower schedule data by config:{}", towerScheduleConfig); } + return data; } @@ -51,28 +53,31 @@ public class TowerScheduleManager { var entranceFloors = getCurrentTowerScheduleData().getEntranceFloorId(); var scheduleFloors = getScheduleFloors(); var nextId = 0; + // find in entrance floors first for(int i=0;i<entranceFloors.size()-1;i++){ if(floorId == entranceFloors.get(i)){ nextId = entranceFloors.get(i+1); } } + if(floorId == entranceFloors.get(entranceFloors.size()-1)){ nextId = scheduleFloors.get(0); } + if(nextId != 0){ return nextId; } + // find in schedule floors - for(int i=0;i<scheduleFloors.size()-1;i++){ + for(int i=0; i < scheduleFloors.size() - 1; i++){ if(floorId == scheduleFloors.get(i)){ - nextId = scheduleFloors.get(i+1); + nextId = scheduleFloors.get(i + 1); } - } - return nextId; + }return nextId; } public Integer getLastEntranceFloor() { - return getCurrentTowerScheduleData().getEntranceFloorId().get(getCurrentTowerScheduleData().getEntranceFloorId().size()-1); + return getCurrentTowerScheduleData().getEntranceFloorId().get(getCurrentTowerScheduleData().getEntranceFloorId().size() - 1); } } diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java index 46c80cd25..95356a15a 100644 --- a/src/main/java/emu/grasscutter/game/world/World.java +++ b/src/main/java/emu/grasscutter/game/world/World.java @@ -1,39 +1,24 @@ package emu.grasscutter.game.world; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; -import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player.SceneLoadState; -import emu.grasscutter.game.props.ClimateType; import emu.grasscutter.game.props.EnterReason; import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.DungeonData; import emu.grasscutter.data.def.SceneData; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityClientGadget; -import emu.grasscutter.game.entity.EntityBaseGadget; import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; -import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; import emu.grasscutter.scripts.data.SceneConfig; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.packet.send.PacketDelTeamEntityNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; import emu.grasscutter.server.packet.send.PacketPlayerEnterSceneNotify; -import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; -import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify; import emu.grasscutter.server.packet.send.PacketScenePlayerInfoNotify; import emu.grasscutter.server.packet.send.PacketSyncScenePlayTeamEntityNotify; import emu.grasscutter.server.packet.send.PacketSyncTeamEntityNotify; diff --git a/src/main/java/emu/grasscutter/plugin/Plugin.java b/src/main/java/emu/grasscutter/plugin/Plugin.java index 97fc5fd77..f1ce18a6b 100644 --- a/src/main/java/emu/grasscutter/plugin/Plugin.java +++ b/src/main/java/emu/grasscutter/plugin/Plugin.java @@ -8,6 +8,8 @@ import java.io.File; import java.io.InputStream; import java.net.URLClassLoader; +import static emu.grasscutter.Configuration.*; + /** * The base class for all plugins to extend. */ @@ -32,7 +34,7 @@ public abstract class Plugin { this.identifier = identifier; this.classLoader = classLoader; - this.dataFolder = new File(Grasscutter.getConfig().PLUGINS_FOLDER, identifier.name); + this.dataFolder = new File(PLUGINS_FOLDER, identifier.name); if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) { Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name); diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java index bc78d12eb..5974cac44 100644 --- a/src/main/java/emu/grasscutter/plugin/PluginManager.java +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -16,6 +16,8 @@ import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import static emu.grasscutter.Configuration.*; + /** * Manages the server's plugins and the event system. */ @@ -31,8 +33,7 @@ public final class PluginManager { * Loads plugins from the config-specified directory. */ private void loadPlugins() { - String directory = Grasscutter.getConfig().PLUGINS_FOLDER; - File pluginsDir = new File(Utils.toFilePath(directory)); + File pluginsDir = new File(Utils.toFilePath(PLUGINS_FOLDER)); if(!pluginsDir.exists() && !pluginsDir.mkdirs()) { Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath()); return; diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 5b86aa298..aa2b9bc51 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -31,6 +31,8 @@ import emu.grasscutter.scripts.data.ScriptArgs; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import static emu.grasscutter.Configuration.*; + public class SceneScriptManager { private final Scene scene; private final ScriptLib scriptLib; @@ -164,7 +166,7 @@ public class SceneScriptManager { private void init() { // Get compiled script if cached CompiledScript cs = ScriptLoader.getScriptByPath( - Grasscutter.getConfig().SCRIPTS_FOLDER + "Scene/" + getScene().getId() + "/scene" + getScene().getId() + "." + ScriptLoader.getScriptType()); + SCRIPT("Scene/" + getScene().getId() + "/scene" + getScene().getId() + "." + ScriptLoader.getScriptType())); if (cs == null) { Grasscutter.getLogger().warn("No script found for scene " + getScene().getId()); @@ -211,7 +213,7 @@ public class SceneScriptManager { private void loadBlockFromScript(SceneBlock block) { CompiledScript cs = ScriptLoader.getScriptByPath( - Grasscutter.getConfig().SCRIPTS_FOLDER + "Scene/" + getScene().getId() + "/scene" + getScene().getId() + "_block" + block.id + "." + ScriptLoader.getScriptType()); + SCRIPT("Scene/" + getScene().getId() + "/scene" + getScene().getId() + "_block" + block.id + "." + ScriptLoader.getScriptType())); if (cs == null) { return; @@ -234,7 +236,7 @@ public class SceneScriptManager { group.setLoaded(true); CompiledScript cs = ScriptLoader.getScriptByPath( - Grasscutter.getConfig().SCRIPTS_FOLDER + "Scene/" + getScene().getId() + "/scene" + getScene().getId() + "_group" + group.id + "." + ScriptLoader.getScriptType()); + SCRIPTS_FOLDER + "Scene/" + getScene().getId() + "/scene" + getScene().getId() + "_group" + group.id + "." + ScriptLoader.getScriptType()); if (cs == null) { return; @@ -281,9 +283,7 @@ public class SceneScriptManager { if (gadget != null) { suite.sceneGadgets.add(gadget); } - } catch (Exception e) { - continue; - } + } catch (Exception ignored) { } } } this.sceneGroups.put(group.id, group); diff --git a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java index 333d8ea21..ab752c8e1 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java @@ -10,26 +10,29 @@ import java.io.FileInputStream; import java.io.IOException; import java.util.Objects; +import static emu.grasscutter.Configuration.*; + public final class AnnouncementHandler implements HttpContextHandler { @Override public void handle(Request request, Response response) throws IOException {//event if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(Grasscutter.getConfig().DATA_FOLDER + "GameAnnouncement.json")) +"}"); + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}"); } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { - String data = readToString(new File(Grasscutter.getConfig().DATA_FOLDER + "GameAnnouncementList.json")).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); + String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); } } + @SuppressWarnings("ResultOfMethodCallIgnored") private static String readToString(File file) { - Long filelength = file.length(); - byte[] filecontent = new byte[filelength.intValue()]; + long length = file.length(); + byte[] content = new byte[(int) length]; try { FileInputStream in = new FileInputStream(file); - in.read(filecontent); - in.close(); - } catch (IOException fileNotFoundException) { - fileNotFoundException.printStackTrace(); + in.read(content); in.close(); + } catch (IOException ignored) { + Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); } - return new String(filecontent); + + return new String(content); } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java index 54ee38988..8d1164e8d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java @@ -11,6 +11,7 @@ import express.http.Request; import express.http.Response; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public final class DispatchHttpJsonHandler implements HttpContextHandler { private final String response; @@ -34,8 +35,8 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { @Override public void handle(Request req, Response res) throws IOException { // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled - if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, req.baseUrl()))) { - Grasscutter.getLogger().info(translate("messages.dispatch.request", req.ip(), req.method(), req.baseUrl()) + (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING ? "(MISSING)" : "")); + if(SERVER.debugLevel == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, req.baseUrl()))) { + Grasscutter.getLogger().info(translate("messages.dispatch.request", req.ip(), req.method(), req.baseUrl()) + (SERVER.debugLevel == ServerDebugMode.MISSING ? "(MISSING)" : "")); } res.send(response); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index f7f14019f..3ae4ea08a 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -4,7 +4,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.protobuf.ByteString; -import emu.grasscutter.Config; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerRunMode; @@ -33,9 +32,11 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import java.io.*; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.*; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public final class DispatchServer { public static String query_region_list = ""; @@ -64,7 +65,7 @@ public final class DispatchServer { public void setHttpServer(Express httpServer) { this.httpServer.stop(); this.httpServer = httpServer; - this.httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); + this.httpServer.listen(DISPATCH_INFO.bindPort); } public Gson getGsonFactory() { @@ -73,7 +74,7 @@ public final class DispatchServer { public QueryCurrRegionHttpRsp getCurrRegion() { // Needs to be fixed by having the game servers connect to the dispatch server. - if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) { + if (SERVER.runMode == ServerRunMode.HYBRID) { return regions.get(defaultServerName).parsedRegionQuery; } @@ -84,14 +85,14 @@ public final class DispatchServer { public void loadQueries() { File file; - file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_region_list.txt"); + file = new File(DATA("query_region_list.txt")); if (file.exists()) { query_region_list = new String(FileUtils.read(file)); } else { Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list."); } - file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt"); + file = new File(DATA("query_cur_region.txt")); if (file.exists()) { query_cur_region = new String(FileUtils.read(file)); } else { @@ -108,52 +109,37 @@ public final class DispatchServer { QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2); List<RegionSimpleInfo> servers = new ArrayList<>(); - List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts - if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) { // Automatically add the game server if in - // hybrid mode + List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts. + if (SERVER.runMode == ServerRunMode.HYBRID) { // Automatically add the game server if in hybrid mode. RegionSimpleInfo server = RegionSimpleInfo.newBuilder() .setName("os_usa") - .setTitle(Grasscutter.getConfig().getGameServerOptions().Name) + .setTitle(DISPATCH_INFO.defaultName) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" - + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() - ? Grasscutter.getConfig().getDispatchOptions().Ip - : Grasscutter.getConfig().getDispatchOptions().PublicIp) - + ":" - + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 - ? Grasscutter.getConfig().getDispatchOptions().PublicPort - : Grasscutter.getConfig().getDispatchOptions().Port) + "http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" + + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + defaultServerName) .build(); usedNames.add(defaultServerName); servers.add(server); RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() - ? Grasscutter.getConfig().getGameServerOptions().Ip - : Grasscutter.getConfig().getGameServerOptions().PublicIp)) - .setGateserverPort(Grasscutter.getConfig().getGameServerOptions().PublicPort != 0 - ? Grasscutter.getConfig().getGameServerOptions().PublicPort - : Grasscutter.getConfig().getGameServerOptions().Port) - .setSecretKey(ByteString - .copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .setGateserverIp(lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress)) + .setGateserverPort(lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)) + .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_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); - } + } else if (DISPATCH_INFO.regions.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()) { + for (var regionInfo : DISPATCH_INFO.regions) { if (usedNames.contains(regionInfo.Name)) { Grasscutter.getLogger().error("Region name already in use."); continue; @@ -163,13 +149,10 @@ public final class DispatchServer { .setTitle(regionInfo.Title) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" - + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() - ? Grasscutter.getConfig().getDispatchOptions().Ip - : Grasscutter.getConfig().getDispatchOptions().PublicIp) - + ":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 - ? Grasscutter.getConfig().getDispatchOptions().PublicPort - : Grasscutter.getConfig().getDispatchOptions().Port) + "/query_cur_region/" + regionInfo.Name) + "http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" + + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + + "/query_cur_region/" + regionInfo.Name) .build(); usedNames.add(regionInfo.Name); servers.add(server); @@ -178,7 +161,7 @@ public final class DispatchServer { .setGateserverIp(regionInfo.Ip) .setGateserverPort(regionInfo.Port) .setSecretKey(ByteString - .copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) .build(); QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); @@ -194,8 +177,8 @@ public final class DispatchServer { .build(); this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray()); - } catch (Exception e) { - Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", e); + } catch (Exception exception) { + Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", exception); } } @@ -205,14 +188,14 @@ public final class DispatchServer { Server server = new Server(); ServerConnector serverConnector; - if(Grasscutter.getConfig().getDispatchOptions().UseSSL) { + if(DISPATCH_ENCRYPTION.useEncryption) { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - File keystoreFile = new File(Grasscutter.getConfig().getDispatchOptions().KeystorePath); + File keystoreFile = new File(DISPATCH_ENCRYPTION.keystore); if(keystoreFile.exists()) { try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword(Grasscutter.getConfig().getDispatchOptions().KeystorePassword); + sslContextFactory.setKeyStorePassword(DISPATCH_ENCRYPTION.keystorePassword); } catch (Exception e) { e.printStackTrace(); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); @@ -230,7 +213,7 @@ public final class DispatchServer { serverConnector = new ServerConnector(server, sslContextFactory); } else { Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); - Grasscutter.getConfig().getDispatchOptions().UseSSL = false; + DISPATCH_ENCRYPTION.useEncryption = false; serverConnector = new ServerConnector(server); } @@ -238,24 +221,27 @@ public final class DispatchServer { serverConnector = new ServerConnector(server); } - serverConnector.setPort(Grasscutter.getConfig().getDispatchOptions().Port); + serverConnector.setPort(DISPATCH_INFO.bindPort); server.setConnectors(new Connector[]{serverConnector}); return server; }); - config.enforceSsl = Grasscutter.getConfig().getDispatchOptions().UseSSL; - if(Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { + config.enforceSsl = DISPATCH_ENCRYPTION.useEncryption; + if(SERVER.debugLevel == ServerDebugMode.ALL) { config.enableDevLogging(); } - if (Grasscutter.getConfig().getDispatchOptions().CORS){ - if (Grasscutter.getConfig().getDispatchOptions().CORSAllowedOrigins.length > 0) config.enableCorsForOrigin(Grasscutter.getConfig().getDispatchOptions().CORSAllowedOrigins); + + if (DISPATCH_POLICIES.cors.enabled) { + var corsPolicy = DISPATCH_POLICIES.cors; + if (corsPolicy.allowedOrigins.length > 0) + config.enableCorsForOrigin(corsPolicy.allowedOrigins); else config.enableCorsForAllOrigins(); } }); httpServer.get("/", (req, res) -> res.send("<!doctype html><html><head><meta charset=\"utf8\"></head><body>" + translate("messages.status.welcome") + "</body></html>")); httpServer.raw().error(404, ctx -> { - if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { + if(SERVER.debugLevel == ServerDebugMode.MISSING) { Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url())); } ctx.contentType("text/html"); @@ -450,7 +436,7 @@ public final class DispatchServer { httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); // gacha record. - String gachaMappingsPath = Utils.toFilePath(Grasscutter.getConfig().DATA_FOLDER + "/gacha_mappings.js"); + String gachaMappingsPath = Utils.toFilePath(DATA("/gacha_mappings.js")); // TODO: Only serve the html page and have a subsequent request to fetch the gacha data. httpServer.get("/gacha", new GachaRecordHandler()); if(!(new File(gachaMappingsPath).exists())) { @@ -462,7 +448,7 @@ public final class DispatchServer { // static file support for plugins httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files - httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port); + httpServer.listen(DISPATCH_INFO.bindPort); Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(httpServer.raw().port()))); } @@ -481,15 +467,11 @@ public final class DispatchServer { 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 + if (eqPos < 0 || eqPos > next) { + result.put(URLDecoder.decode(qs.substring(last, next), StandardCharsets.UTF_8), ""); + } else { + result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8), + URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8)); } } last = next + 1; diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java index e5a4ca055..67b3d4023 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java @@ -9,6 +9,7 @@ import express.http.Request; import express.http.Response; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public class DefaultAuthenticationHandler implements AuthenticationHandler { @@ -37,11 +38,11 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { // Check if account exists, else create a new one. if (account == null) { // Account doesn't exist, so we can either auto create it if the config value is set. - if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) { + if (ACCOUNT.autoCreate) { // This account has been created AUTOMATICALLY. There will be no permissions added. account = DatabaseHelper.createAccountWithId(requestData.account, 0); - for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { + for (String permission : ACCOUNT.defaultPermissions) { account.addPermission(permission); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java index 8676574bb..b90510367 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java @@ -3,7 +3,6 @@ package emu.grasscutter.server.dispatch.http; import java.io.File; import java.io.IOException; -import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; import emu.grasscutter.utils.FileUtils; @@ -12,10 +11,12 @@ import express.http.HttpContextHandler; import express.http.Request; import express.http.Response; +import static emu.grasscutter.Configuration.*; + public final class GachaRecordHandler implements HttpContextHandler { String render_template; public GachaRecordHandler() { - File template = new File(Utils.toFilePath(Grasscutter.getConfig().DATA_FOLDER + "/gacha_records.html")); + File template = new File(Utils.toFilePath(DATA("/gacha_records.html"))); if (template.exists()) { // Load from cache render_template = new String(FileUtils.read(template)); @@ -31,11 +32,11 @@ public final class GachaRecordHandler implements HttpContextHandler { int page = 0; int gachaType = 0; if (req.query("p") != null) { - page = Integer.valueOf(req.query("p")); + page = Integer.parseInt(req.query("p")); } if (req.query("gachaType") != null) { - gachaType = Integer.valueOf(req.query("gachaType")); + gachaType = Integer.parseInt(req.query("gachaType")); } Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index cb0e4965d..71e1cf856 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -30,11 +30,9 @@ import java.net.InetSocketAddress; import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public final class GameServer extends KcpServer { private final InetSocketAddress address; @@ -59,8 +57,8 @@ public final class GameServer extends KcpServer { public GameServer() { this(new InetSocketAddress( - Grasscutter.getConfig().getGameServerOptions().Ip, - Grasscutter.getConfig().getGameServerOptions().Port + GAME_INFO.bindAddress, + GAME_INFO.bindPort )); } diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 88e7fa17f..4bba854ef 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -14,6 +14,8 @@ import emu.grasscutter.server.game.GameSession.SessionState; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import static emu.grasscutter.Configuration.*; + @SuppressWarnings("unchecked") public class GameServerPacketHandler { private final Int2ObjectMap<PacketHandler> handlers; @@ -92,7 +94,7 @@ public class GameServerPacketHandler { } // Log unhandled packets - if (Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) { + if (SERVER.debugLevel == ServerDebugMode.MISSING) { Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + emu.grasscutter.net.packet.PacketOpcodesUtil.getOpcodeName(opcode)); } } diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index d1d7eef01..7cc9a799f 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -3,7 +3,6 @@ package emu.grasscutter.server.game; import java.io.File; import java.net.InetSocketAddress; import java.nio.ByteBuffer; -import java.util.HashSet; import java.util.Set; import emu.grasscutter.Grasscutter; @@ -23,9 +22,10 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public class GameSession extends KcpChannel { - private GameServer server; + private final GameServer server; private Account account; private Player player; @@ -140,7 +140,7 @@ public class GameSession extends KcpChannel { } public void replayPacket(int opcode, String name) { - String filePath = Grasscutter.getConfig().PACKETS_FOLDER + name; + String filePath = PACKETS_FOLDER + name; File p = new File(filePath); if (!p.exists()) return; @@ -172,7 +172,7 @@ public class GameSession extends KcpChannel { } // Log - if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { + if (SERVER.debugLevel == ServerDebugMode.ALL) { logPacket(packet); } @@ -239,7 +239,7 @@ public class GameSession extends KcpChannel { } // Log packet - if (Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) { + if (SERVER.debugLevel == ServerDebugMode.ALL) { if (!loopPacket.contains(opcode)) { 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/HandlerSetPlayerBornDataReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java index 53d141a99..ec591c91f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerBornDataReq.java @@ -19,6 +19,8 @@ import emu.grasscutter.server.game.GameSession.SessionState; import java.util.Arrays; +import static emu.grasscutter.Configuration.*; + @Opcodes(PacketOpcodes.SetPlayerBornDataReq) public class HandlerSetPlayerBornDataReq extends PacketHandler { @@ -85,12 +87,13 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler { session.send(new BasePacket(PacketOpcodes.SetPlayerBornDataRsp)); // Default mail + var welcomeMail = GAME_INFO.joinOptions.welcomeMail; MailBuilder mailBuilder = new MailBuilder(player.getUid(), new Mail()); - mailBuilder.mail.mailContent.title = Grasscutter.getConfig().GameServer.WelcomeMailTitle; - mailBuilder.mail.mailContent.sender = Grasscutter.getConfig().GameServer.WelcomeMailSender; + mailBuilder.mail.mailContent.title = welcomeMail.title; + mailBuilder.mail.mailContent.sender = welcomeMail.sender; // Please credit Grasscutter if changing something here. We don't condone commercial use of the project. - mailBuilder.mail.mailContent.content = Grasscutter.getConfig().GameServer.WelcomeMailContent + "\n<type=\"browser\" text=\"GitHub\" href=\"https://github.com/Melledy/Grasscutter\"/>"; - mailBuilder.mail.itemList.addAll(Arrays.asList(Grasscutter.getConfig().GameServer.WelcomeMailItems)); + mailBuilder.mail.mailContent.content = welcomeMail.content + "\n<type=\"browser\" text=\"GitHub\" href=\"https://github.com/Melledy/Grasscutter\"/>"; + mailBuilder.mail.itemList.addAll(Arrays.asList(welcomeMail.items)); mailBuilder.mail.importance = 1; player.sendMail(mailBuilder.mail); } catch (Exception e) { 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 ff373140b..a0948c737 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java @@ -1,7 +1,6 @@ package emu.grasscutter.server.packet.send; import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.game.friends.Friendship; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; @@ -12,20 +11,23 @@ import emu.grasscutter.net.proto.GetPlayerFriendListRspOuterClass.GetPlayerFrien import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.PlatformTypeOuterClass; +import static emu.grasscutter.Configuration.*; + public class PacketGetPlayerFriendListRsp extends BasePacket { public PacketGetPlayerFriendListRsp(Player player) { super(PacketOpcodes.GetPlayerFriendListRsp); + var serverAccount = GAME_INFO.serverAccount; FriendBrief serverFriend = FriendBrief.newBuilder() .setUid(GameConstants.SERVER_CONSOLE_UID) - .setNickname(Grasscutter.getConfig().getGameServerOptions().ServerNickname) - .setLevel(Grasscutter.getConfig().getGameServerOptions().ServerLevel) - .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(Grasscutter.getConfig().getGameServerOptions().ServerAvatarId)) - .setWorldLevel(Grasscutter.getConfig().getGameServerOptions().ServerWorldLevel) - .setSignature(Grasscutter.getConfig().getGameServerOptions().ServerSignature) + .setNickname(serverAccount.nickName) + .setLevel(serverAccount.adventureRank) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(serverAccount.avatarId)) + .setWorldLevel(serverAccount.worldLevel) + .setSignature(serverAccount.signature) .setLastActiveTime((int) (System.currentTimeMillis() / 1000f)) - .setNameCardId(Grasscutter.getConfig().getGameServerOptions().ServerNameCardId) + .setNameCardId(serverAccount.nameCardId) .setOnlineState(FriendOnlineState.FRIEND_ONLINE) .setParam(1) .setIsGameSource(true) @@ -37,10 +39,12 @@ public class PacketGetPlayerFriendListRsp extends BasePacket { for (Friendship friendship : player.getFriendsList().getFriends().values()) { proto.addFriendList(friendship.toProto()); } + for (Friendship friendship : player.getFriendsList().getPendingFriends().values()) { if (friendship.getAskerId() == player.getUid()) { continue; } + proto.addAskFriendList(friendship.toProto()); } 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 6407e9412..9a21e4143 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -2,7 +2,6 @@ package emu.grasscutter.server.packet.send; import com.google.protobuf.ByteString; import emu.grasscutter.Grasscutter; -import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; @@ -13,9 +12,10 @@ import emu.grasscutter.server.game.GameSession; import emu.grasscutter.utils.FileUtils; import java.io.File; -import java.net.URL; import java.util.Base64; +import static emu.grasscutter.Configuration.*; + public class PacketPlayerLoginRsp extends BasePacket { private static QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp regionCache; @@ -27,10 +27,10 @@ public class PacketPlayerLoginRsp extends BasePacket { RegionInfo info; - if (Grasscutter.getConfig().RunMode == ServerRunMode.GAME_ONLY) { + if (SERVER.runMode == ServerRunMode.GAME_ONLY) { if (regionCache == null) { try { - File file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt"); + File file = new File(DATA("query_cur_region.txt")); String query_cur_region = ""; if (file.exists()) { query_cur_region = new String(FileUtils.read(file)); @@ -42,9 +42,9 @@ public class PacketPlayerLoginRsp extends BasePacket { QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp.parseFrom(decodedCurRegion); RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getGameServerOptions().Ip : Grasscutter.getConfig().getGameServerOptions().PublicIp)) - .setGateserverPort(Grasscutter.getConfig().getGameServerOptions().PublicPort != 0 ? Grasscutter.getConfig().getGameServerOptions().PublicPort : Grasscutter.getConfig().getGameServerOptions().Port) - .setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin"))) + .setGateserverIp(lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress)) + .setGateserverPort(lr(GAME_INFO.accessPort, GAME_INFO.bindPort)) + .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) .build(); regionCache = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); 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 a3a6ff7b8..3ad196af3 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerStoreNotify.java @@ -1,7 +1,5 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; @@ -10,6 +8,8 @@ import emu.grasscutter.net.proto.ItemOuterClass.Item; import emu.grasscutter.net.proto.PlayerStoreNotifyOuterClass.PlayerStoreNotify; import emu.grasscutter.net.proto.StoreTypeOuterClass.StoreType; +import static emu.grasscutter.Configuration.*; + public class PacketPlayerStoreNotify extends BasePacket { public PacketPlayerStoreNotify(Player player) { @@ -19,7 +19,7 @@ public class PacketPlayerStoreNotify extends BasePacket { PlayerStoreNotify.Builder p = PlayerStoreNotify.newBuilder() .setStoreType(StoreType.STORE_PACK) - .setWeightLimit(Grasscutter.getConfig().getGameServerOptions().InventoryLimitAll); + .setWeightLimit(GAME_OPTIONS.inventoryLimits.all); for (GameItem 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 871534b53..0e757d11b 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPullRecentChatRsp.java @@ -1,42 +1,41 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.Config.GameServerOptions; import emu.grasscutter.game.player.Player; import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.ChatInfoOuterClass.ChatInfo; import emu.grasscutter.net.proto.PullRecentChatRspOuterClass.PullRecentChatRsp; import emu.grasscutter.utils.Utils; +import static emu.grasscutter.Configuration.*; + public class PacketPullRecentChatRsp extends BasePacket { public PacketPullRecentChatRsp(Player player) { super(PacketOpcodes.PullRecentChatRsp); - GameServerOptions serverOptions = Grasscutter.getConfig().getGameServerOptions(); + var joinOptions = GAME_INFO.joinOptions; PullRecentChatRsp.Builder proto = PullRecentChatRsp.newBuilder(); - if (serverOptions.WelcomeEmotes != null && serverOptions.WelcomeEmotes.length > 0) { + if (joinOptions.welcomeEmotes != null && joinOptions.welcomeEmotes.length > 0) { ChatInfo welcomeEmote = ChatInfo.newBuilder() .setTime((int) (System.currentTimeMillis() / 1000)) .setUid(GameConstants.SERVER_CONSOLE_UID) .setToUid(player.getUid()) - .setIcon(serverOptions.WelcomeEmotes[Utils.randomRange(0, serverOptions.WelcomeEmotes.length - 1)]) + .setIcon(joinOptions.welcomeEmotes[Utils.randomRange(0, joinOptions.welcomeEmotes.length - 1)]) .build(); proto.addChatInfo(welcomeEmote); } - if (serverOptions.WelcomeMotd != null && serverOptions.WelcomeMotd.length() > 0) { - ChatInfo welcomeMotd = ChatInfo.newBuilder() + if (joinOptions.welcomeMessage != null && joinOptions.welcomeMessage.length() > 0) { + ChatInfo welcomeMessage = ChatInfo.newBuilder() .setTime((int) (System.currentTimeMillis() / 1000)) .setUid(GameConstants.SERVER_CONSOLE_UID) .setToUid(player.getUid()) - .setText(Grasscutter.getConfig().getGameServerOptions().WelcomeMotd) + .setText(joinOptions.welcomeMessage) .build(); - - proto.addChatInfo(welcomeMotd); + proto.addChatInfo(welcomeMessage); } this.setData(proto); 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 61b51948b..77f9da803 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketStoreWeightLimitNotify.java @@ -1,11 +1,12 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.StoreTypeOuterClass.StoreType; import emu.grasscutter.net.proto.StoreWeightLimitNotifyOuterClass.StoreWeightLimitNotify; +import static emu.grasscutter.Configuration.*; + public class PacketStoreWeightLimitNotify extends BasePacket { public PacketStoreWeightLimitNotify() { @@ -13,11 +14,11 @@ public class PacketStoreWeightLimitNotify extends BasePacket { StoreWeightLimitNotify p = StoreWeightLimitNotify.newBuilder() .setStoreType(StoreType.STORE_PACK) - .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) + .setWeightLimit(INVENTORY_LIMITS.all) + .setWeaponCountLimit(INVENTORY_LIMITS.weapons) + .setReliquaryCountLimit(INVENTORY_LIMITS.relics) + .setMaterialCountLimit(INVENTORY_LIMITS.materials) + .setFurnitureCountLimit(INVENTORY_LIMITS.furniture) .build(); this.setData(p); diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index f69aafc80..4a5af6e49 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -1,12 +1,8 @@ package emu.grasscutter.tools; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.FilenameFilter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; @@ -14,14 +10,12 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.stream.Collectors; import com.google.gson.reflect.TypeToken; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; -import emu.grasscutter.command.CommandHandler; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; @@ -32,6 +26,7 @@ import emu.grasscutter.data.def.SceneData; import emu.grasscutter.utils.Utils; import static emu.grasscutter.utils.Language.translate; +import static emu.grasscutter.Configuration.*; public final class Tools { public static void createGmHandbook() throws Exception { @@ -42,50 +37,45 @@ public final class Tools { ToolsWithLanguageOption.createGachaMapping(location, getLanguageOption()); } - public static List<String> getAvailableLanguage() throws Exception { - File textMapFolder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap"); - List<String> availableLangList = new ArrayList<String>(); - for (String textMapFileName : textMapFolder.list(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - if (name.startsWith("TextMap") && name.endsWith(".json")){ - return true; - } - return false; - } - })) { - availableLangList.add(textMapFileName.replace("TextMap","").replace(".json","").toLowerCase()); - } - return availableLangList; + public static List<String> getAvailableLanguage() { + File textMapFolder = new File(RESOURCE("TextMap")); + List<String> availableLangList = new ArrayList<>(); + for (String textMapFileName : Objects.requireNonNull(textMapFolder.list((dir, name) -> name.startsWith("TextMap") && name.endsWith(".json")))) { + availableLangList.add(textMapFileName.replace("TextMap", "").replace(".json", "").toLowerCase()); + } return availableLangList; } - public static String getLanguageOption() throws Exception { + public static String getLanguageOption() { List<String> availableLangList = getAvailableLanguage(); // Use system out for better format if (availableLangList.size() == 1) { return availableLangList.get(0).toUpperCase(); } - String stagedMessage = ""; - stagedMessage += "The following languages mappings are available, please select one: [default: EN]\n"; - String groupedLangList = ">\t"; + StringBuilder stagedMessage = new StringBuilder(); + stagedMessage.append("The following languages mappings are available, please select one: [default: EN] \n"); + + StringBuilder groupedLangList = new StringBuilder(">\t"); String input; int groupedLangCount = 0; - String input = ""; + for (String availableLanguage: availableLangList){ groupedLangCount++; - groupedLangList = groupedLangList + "" + availableLanguage + "\t"; + groupedLangList.append(availableLanguage).append("\t"); + if (groupedLangCount == 6) { - stagedMessage += groupedLangList + "\n"; + stagedMessage.append(groupedLangList).append("\n"); groupedLangCount = 0; - groupedLangList = ">\t"; + groupedLangList = new StringBuilder(">\t"); } } - if (groupedLangCount > 0) { - stagedMessage += groupedLangList + "\n"; - } - stagedMessage += "\nYour choice:[EN] "; - input = Grasscutter.getConsole().readLine(stagedMessage); + if (groupedLangCount > 0) { + stagedMessage.append(groupedLangList).append("\n"); + } + + stagedMessage.append("\nYour choice:[EN] "); + + input = Grasscutter.getConsole().readLine(stagedMessage.toString()); if (availableLangList.contains(input.toLowerCase())) { return input.toUpperCase(); } @@ -101,7 +91,7 @@ final class ToolsWithLanguageOption { ResourceLoader.loadResources(); Map<Long, String> map; - try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) { + try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(RESOURCE("TextMap/TextMap"+language+".json"))), StandardCharsets.UTF_8)) { map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType()); } @@ -119,9 +109,9 @@ final class ToolsWithLanguageOption { writer.println("// Commands"); for (Command cmd : cmdList) { - String cmdName = cmd.label(); + StringBuilder cmdName = new StringBuilder(cmd.label()); while (cmdName.length() <= 15) { - cmdName = " " + cmdName; + cmdName.insert(0, " "); } writer.println(cmdName + " : " + translate(cmd.description())); } @@ -178,16 +168,13 @@ final class ToolsWithLanguageOption { ResourceLoader.loadResources(); Map<Long, String> map; - try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8)) { + try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(RESOURCE("TextMap/TextMap" + language + ".json"))), StandardCharsets.UTF_8)) { map = Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<Long, String>>() {}.getType()); } List<Integer> list; - String fileName = location; - - try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) { - + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(location), StandardCharsets.UTF_8), false)) { list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); Collections.sort(list); @@ -209,18 +196,11 @@ final class ToolsWithLanguageOption { } else { writer.print(","); } - String color; - switch (data.getQualityType()){ - case "QUALITY_PURPLE": - color = "purple"; - break; - case "QUALITY_ORANGE": - color = "yellow"; - break; - case "QUALITY_BLUE": - default: - color = "blue"; - } + String color = switch (data.getQualityType()) { + case "QUALITY_PURPLE" -> "purple"; + case "QUALITY_ORANGE" -> "yellow"; + default -> "blue"; + }; // Got the magic number 4233146695 from manually search in the json file writer.println( "\"" + (avatarID % 1000 + 1000) + "\" : [\"" diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index e6d260e94..188a7192e 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -7,6 +7,8 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.net.proto.GetPlayerTokenRspOuterClass.GetPlayerTokenRsp; import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; +import static emu.grasscutter.Configuration.*; + public final class Crypto { private static final SecureRandom secureRandom = new SecureRandom(); public static final long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968"); @@ -16,9 +18,9 @@ public final class Crypto { public static byte[] ENCRYPT_KEY; public static void loadKeys() { - DISPATCH_KEY = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchKey.bin"); - ENCRYPT_KEY = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "secretKey.bin"); - ENCRYPT_SEED_BUFFER = FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "secretKeyBuffer.bin"); + DISPATCH_KEY = FileUtils.read(KEYS_FOLDER + "/dispatchKey.bin"); + ENCRYPT_KEY = FileUtils.read(KEYS_FOLDER + "/secretKey.bin"); + ENCRYPT_SEED_BUFFER = FileUtils.read(KEYS_FOLDER + "/secretKeyBuffer.bin"); } public static void xor(byte[] packet, byte[] key) { @@ -34,7 +36,7 @@ public final class Crypto { public static void extractSecretKeyBuffer(byte[] data) { try { GetPlayerTokenRsp p = GetPlayerTokenRsp.parseFrom(data); - FileUtils.write(Grasscutter.getConfig().KEY_FOLDER + "secretKeyBuffer.bin", p.getSecretKeyBytes().toByteArray()); + FileUtils.write(KEYS_FOLDER + "/secretKeyBuffer.bin", p.getSecretKeyBytes().toByteArray()); Grasscutter.getLogger().info("Secret Key: " + p.getSecretKey()); } catch (Exception e) { Grasscutter.getLogger().error("Crypto error.", e); @@ -44,7 +46,7 @@ public final class Crypto { public static void extractDispatchSeed(String data) { try { QueryCurrRegionHttpRsp p = QueryCurrRegionHttpRsp.parseFrom(Base64.getDecoder().decode(data)); - FileUtils.write(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin", p.getRegionInfo().getSecretKey().toByteArray()); + FileUtils.write(KEYS_FOLDER + "/dispatchSeed.bin", p.getRegionInfo().getSecretKey().toByteArray()); } catch (Exception e) { Grasscutter.getLogger().error("Crypto error.", e); } diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 7c3426384..4698606d7 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -10,6 +10,8 @@ import java.io.InputStream; import java.util.concurrent.ConcurrentHashMap; import java.util.Map; +import static emu.grasscutter.Configuration.*; + public final class Language { private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>(); @@ -27,8 +29,8 @@ public final class Language { return cachedLanguages.get(langCode); } - var fallbackLanguageCode = Utils.getLanguageCode(Grasscutter.getConfig().DefaultLanguage); - var description = getLanguageFileStreamDescripter(langCode, fallbackLanguageCode); + var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE); + var description = getLanguageFileDescription(langCode, fallbackLanguageCode); var actualLanguageCode = description.getLanguageCode(); Language languageInst; @@ -111,7 +113,7 @@ public final class Language { * @param languageCode The name of the language code. * @param fallbackLanguageCode The name of the fallback language code. */ - private static LanguageStreamDescription getLanguageFileStreamDescripter(String languageCode, String fallbackLanguageCode) { + private static LanguageStreamDescription getLanguageFileDescription(String languageCode, String fallbackLanguageCode) { var fileName = languageCode + ".json"; var fallback = fallbackLanguageCode + ".json"; diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 764993255..648419b51 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -12,6 +12,7 @@ import java.util.Random; import java.util.Locale; import emu.grasscutter.Config; +import emu.grasscutter.Configuration; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -175,12 +176,12 @@ public final class Utils { * Checks for required files and folders before startup. */ public static void startupCheck() { - Config config = Grasscutter.getConfig(); + Configuration config = Grasscutter.getConfig(); Logger logger = Grasscutter.getLogger(); boolean exit = false; - String resourcesFolder = config.RESOURCE_FOLDER; - String dataFolder = config.DATA_FOLDER; + String resourcesFolder = config.folderStructure.resources; + String dataFolder = config.folderStructure.data; // Check for resources folder. if(!fileExists(resourcesFolder)) { From d5cc615948ff2493e9a0714c89fca47b39dbb33e Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 00:46:49 -0400 Subject: [PATCH 263/434] Remove old config & migrate legacy configs --- src/main/java/emu/grasscutter/Configuration.java | 13 ++++++++++++- src/main/java/emu/grasscutter/Grasscutter.java | 4 +++- src/main/java/emu/grasscutter/utils/Utils.java | 1 - 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 369160f42..7f7bf16fe 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -1,8 +1,10 @@ package emu.grasscutter; +import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter.*; import emu.grasscutter.game.mail.Mail.*; +import java.io.FileReader; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Locale; @@ -24,7 +26,16 @@ public final class Configuration { * Attempts to update the server's existing configuration to the latest configuration. */ public static void updateConfig() { - var existing = config.version; + try { // Check if the server is using a legacy config. + JsonObject configObject = Grasscutter.getGsonFactory() + .fromJson(new FileReader(Grasscutter.configFile), JsonObject.class); + if(!configObject.has("version")) { + Grasscutter.getLogger().info("Updating legacy configuration..."); + Grasscutter.saveConfig(null); + } + } catch (Exception ignored) { } + + var existing = config.version; var latest = version(); if(existing == latest) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 07cc1f240..6ba8bd986 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -43,7 +43,7 @@ public final class Grasscutter { private static Language language; private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - private static final File configFile = new File("./config.json"); + public static final File configFile = new File("./config.json"); private static int day; // Current day of week. @@ -60,6 +60,8 @@ public final class Grasscutter { // Load server configuration. config = Grasscutter.loadConfig(); + // Attempt to update configuration. + Configuration.updateConfig(); // Load translation files. Grasscutter.loadLanguage(); diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 648419b51..6b8365170 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -11,7 +11,6 @@ import java.util.Map; import java.util.Random; import java.util.Locale; -import emu.grasscutter.Config; import emu.grasscutter.Configuration; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; From 145546c26ce631fd5811159b49e945b6509eda90 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 00:48:40 -0400 Subject: [PATCH 264/434] Remove config file --- src/main/java/emu/grasscutter/Config.java | 110 ---------------------- 1 file changed, 110 deletions(-) delete mode 100644 src/main/java/emu/grasscutter/Config.java diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java deleted file mode 100644 index 4ec16e0d1..000000000 --- a/src/main/java/emu/grasscutter/Config.java +++ /dev/null @@ -1,110 +0,0 @@ -package emu.grasscutter; - -import java.util.Locale; -import emu.grasscutter.Grasscutter.ServerDebugMode; -import emu.grasscutter.Grasscutter.ServerRunMode; -import emu.grasscutter.game.mail.Mail; - -public final class Config { - 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 String SCRIPTS_FOLDER = "./resources/Scripts/"; - public String PLUGINS_FOLDER = "./plugins/"; - - public ServerDebugMode DebugMode = ServerDebugMode.NONE; // ALL, MISSING, NONE - public ServerRunMode RunMode = ServerRunMode.HYBRID; // HYBRID, DISPATCH_ONLY, GAME_ONLY - public GameServerOptions GameServer = new GameServerOptions(); - public DispatchServerOptions DispatchServer = new DispatchServerOptions(); - public Locale LocaleLanguage = Locale.getDefault(); - public Locale DefaultLanguage = Locale.US; - - public Boolean OpenStamina = true; - 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 int PublicPort = 0; - public String KeystorePath = "./keystore.p12"; - public String KeystorePassword = "123456"; - public Boolean UseSSL = true; - public Boolean FrontHTTPS = true; - public Boolean CORS = false; - public String[] CORSAllowedOrigins = new String[] { "*" }; - - public boolean AutomaticallyCreateAccounts = false; - public String[] defaultPermissions = new String[] { "" }; - - 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 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 int PublicPort = 0; - - public String DispatchServerDatabaseUrl = "mongodb://localhost:27017"; - public String DispatchServerDatabaseCollection = "grasscutter"; - - public int InventoryLimitWeapon = 2000; - public int InventoryLimitRelic = 2000; - public int InventoryLimitMaterial = 2000; - public int InventoryLimitFurniture = 2000; - public int InventoryLimitAll = 30000; - 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 String ServerNickname = "Server"; - public int ServerAvatarId = 10000007; - public int ServerNameCardId = 210001; - public int ServerLevel = 1; - public int ServerWorldLevel = 1; - public String ServerSignature = "Server Signature"; - public int[] WelcomeEmotes = {2007, 1002, 4010}; - public String WelcomeMotd = "Welcome to Grasscutter emu"; - public String WelcomeMailTitle = "Welcome to Grasscutter!"; - public String WelcomeMailSender = "Lawnmower"; - public String WelcomeMailContent = "Hi there!\r\nFirst of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r\n\r\nCheck out our:\r\n<type=\"browser\" text=\"Discord\" href=\"https://discord.gg/T5vZU6UyeG\"/>"; - public Mail.MailItem[] WelcomeMailItems = { - new Mail.MailItem(13509, 1, 1), - new Mail.MailItem(201, 10000, 1), - }; - - public boolean EnableOfficialShop = true; - - 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; - } - } -} From 5c0e193a0a969e1e30cb9fb282bc24a6af8d5cbb Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 01:23:18 -0400 Subject: [PATCH 265/434] Fix `ClassDefNotFound` & `NullPointerException` errors --- .../java/emu/grasscutter/Configuration.java | 227 +---------------- .../java/emu/grasscutter/Grasscutter.java | 45 ++-- .../command/commands/ReloadCommand.java | 2 +- .../grasscutter/utils/ConfigContainer.java | 229 ++++++++++++++++++ .../java/emu/grasscutter/utils/Utils.java | 3 +- 5 files changed, 249 insertions(+), 257 deletions(-) create mode 100644 src/main/java/emu/grasscutter/utils/ConfigContainer.java diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 7f7bf16fe..7adc334c1 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -1,12 +1,7 @@ package emu.grasscutter; -import com.google.gson.JsonObject; -import emu.grasscutter.Grasscutter.*; -import emu.grasscutter.game.mail.Mail.*; +import emu.grasscutter.utils.ConfigContainer; -import java.io.FileReader; -import java.lang.reflect.Field; -import java.util.Arrays; import java.util.Locale; import static emu.grasscutter.Grasscutter.config; @@ -17,56 +12,14 @@ import static emu.grasscutter.Grasscutter.config; * Use `import static emu.grasscutter.Configuration.*;` * to import all configuration constants. */ -public final class Configuration { - private static int version() { - return 1; - } - - /** - * Attempts to update the server's existing configuration to the latest configuration. - */ - public static void updateConfig() { - try { // Check if the server is using a legacy config. - JsonObject configObject = Grasscutter.getGsonFactory() - .fromJson(new FileReader(Grasscutter.configFile), JsonObject.class); - if(!configObject.has("version")) { - Grasscutter.getLogger().info("Updating legacy configuration..."); - Grasscutter.saveConfig(null); - } - } catch (Exception ignored) { } - - var existing = config.version; - var latest = version(); - - if(existing == latest) - return; - - // Create a new configuration instance. - Configuration updated = new Configuration(); - // Update all configuration fields. - Field[] fields = Configuration.class.getDeclaredFields(); - Arrays.stream(fields).forEach(field -> { - try { - field.set(updated, field.get(config)); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to update a configuration field.", exception); - } - }); - - try { // Save configuration & reload. - Grasscutter.saveConfig(updated); - Grasscutter.reloadConfig(); - } catch (Exception exception) { - Grasscutter.getLogger().warn("Failed to inject the updated configuration.", exception); - } - } +public final class Configuration extends ConfigContainer { /* * Constants */ // 'c' is short for 'config' and makes code look 'cleaner'. - public static final Configuration c = config; + public static final ConfigContainer c = config; public static final Locale LANGUAGE = config.language.language; public static final Locale FALLBACK_LANGUAGE = config.language.fallback; @@ -135,178 +88,4 @@ public final class Configuration { public static int lr(int left, int right) { return left == 0 ? right : left; } - - /* - * Configuration data. - */ - - public Structure folderStructure; - public Database databaseInfo; - public Language language; - public Account account; - public Server server; - - // DO NOT. TOUCH. THE VERSION NUMBER. - public int version = version(); - - /* Option containers. */ - - public static class Database { - public String connectionUri = "mongodb://localhost:27017"; - public String collection = "grasscutter"; - } - - public static class Structure { - public String resources = "./resources/"; - public String data = "./data/"; - public String packets = "./packets/"; - public String keys = "./keys/"; - public String scripts = "./resources/scripts/"; - public String plugins = "./plugins/"; - - // UNUSED (potentially added later?) - // public String dumps = "./dumps/"; - } - - public static class Server { - public ServerDebugMode debugLevel = ServerDebugMode.NONE; - public ServerRunMode runMode = ServerRunMode.HYBRID; - - public Dispatch dispatch = new Dispatch(); - public Game game = new Game(); - } - - public static class Language { - public Locale language = Locale.getDefault(); - public Locale fallback = Locale.US; - } - - public static class Account { - public boolean autoCreate = false; - public String[] defaultPermissions = {}; - } - - /* Server options. */ - - public static class Dispatch { - public String bindAddress = "0.0.0.0"; - /* This is the address used in URLs. */ - public String accessAddress = "127.0.0.1"; - - public int bindPort = 443; - /* This is the port used in URLs. */ - public int accessPort = 443; - - public Encryption encryption = new Encryption(); - public Policies policies = new Policies(); - public Region[] regions = {}; - - public String defaultName = "Grasscutter"; - } - - public static class Game { - public String bindAddress = "0.0.0.0"; - /* This is the address used in the default region. */ - public String accessAddress = "127.0.0.1"; - - public int bindPort = 443; - /* This is the port used in the default region. */ - public int accessPort = 443; - - public GameOptions gameOptions = new GameOptions(); - public JoinOptions joinOptions = new JoinOptions(); - public ConsoleAccount serverAccount = new ConsoleAccount(); - } - - /* Data containers. */ - - public static class Encryption { - public boolean useEncryption = true; - /* Should 'https' be appended to URLs? */ - public boolean useInRouting = true; - public String keystore = "./keystore.p12"; - public String keystorePassword = "123456"; - } - - public static class Policies { - public CORS cors = new CORS(); - - public static class CORS { - public boolean enabled = false; - public String[] allowedOrigins = new String[]{"*"}; - } - } - - public static class GameOptions { - public InventoryLimits inventoryLimits = new InventoryLimits(); - public AvatarLimits avatarLimits = new AvatarLimits(); - public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. - - public boolean watchGachaConfig = false; - public boolean enableShopItems = true; - public boolean staminaUsage = true; - public Rates rates = new Rates(); - - public Database databaseInfo = new Database(); - - public static class InventoryLimits { - public int weapons = 2000; - public int relics = 2000; - public int materials = 2000; - public int furniture = 2000; - public int all = 30000; - } - - public static class AvatarLimits { - public int singlePlayerTeam = 4; - public int multiplayerTeam = 4; - } - - public static class Rates { - public float adventureExp = 1.0f; - public float mora = 1.0f; - public float leyLines = 1.0f; - } - } - - public static class JoinOptions { - public int[] welcomeEmotes = {2007, 1002, 4010}; - public String welcomeMessage = "Welcome to a Grasscutter server."; - public Mail welcomeMail = new Mail(); - - public static class Mail { - public String title = "Welcome to Grasscutter!"; - public String content = """ - Hi there!\r - First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r - \r - Check out our:\r - <type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/> - """; - public String sender = "Lawnmower"; - public MailItem[] items = { - new MailItem(13509, 1, 1), - new MailItem(201, 99999, 1) - }; - } - } - - public static class ConsoleAccount { - public int avatarId = 10000007; - public int nameCardId = 210001; - public int adventureRank = 1; - public int worldLevel = 0; - - public String nickName = "Server"; - public String signature = "Welcome to Grasscutter!"; - } - - /* Objects. */ - - public static class Region { - public String Name = "os_usa"; - public String Title = "Grasscutter"; - public String Ip = "127.0.0.1"; - public int Port = 22102; - } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 6ba8bd986..73e761e6e 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -1,14 +1,13 @@ package emu.grasscutter; import java.io.*; -import java.lang.reflect.Field; -import java.util.Arrays; import java.util.Calendar; import emu.grasscutter.command.CommandMap; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; import org.jline.reader.EndOfFileException; import org.jline.reader.LineReader; @@ -52,16 +51,16 @@ public final class Grasscutter { private static PluginManager pluginManager; public static final Reflections reflector = new Reflections("emu.grasscutter"); - public static final Configuration config; + public static ConfigContainer config; static { // Declare logback configuration. System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); // Load server configuration. - config = Grasscutter.loadConfig(); + Grasscutter.loadConfig(); // Attempt to update configuration. - Configuration.updateConfig(); + ConfigContainer.updateConfig(); // Load translation files. Grasscutter.loadLanguage(); @@ -144,34 +143,20 @@ public final class Grasscutter { /** * Attempts to load the configuration from a file. - * @return The config from the file, or a new instance. */ - public static Configuration loadConfig() { + public static void loadConfig() { try (FileReader file = new FileReader(configFile)) { - return gson.fromJson(file, Configuration.class); - } catch (Exception e) { + config = gson.fromJson(file, ConfigContainer.class); + } catch (Exception exception) { Grasscutter.saveConfig(null); - return new Configuration(); + config = new ConfigContainer(); + } catch (Error error) { + // Occurred probably from an outdated config file. + Grasscutter.saveConfig(null); + config = new ConfigContainer(); } } - /** - * Attempts to reload the configuration from the file. - * Uses reflection to **replace** the fields in the config. - */ - public static void reloadConfig() { - Configuration fileConfig = Grasscutter.loadConfig(); - - Field[] fields = Configuration.class.getDeclaredFields(); - Arrays.stream(fields).forEach(field -> { - try { - field.set(config, field.get(fileConfig)); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to update a configuration field.", exception); - } - }); - } - public static void loadLanguage() { var locale = config.language.language; language = Language.getLanguage(Utils.getLanguageCode(locale)); @@ -181,8 +166,8 @@ public final class Grasscutter { * Saves the provided server configuration. * @param config The configuration to save, or null for a new one. */ - public static void saveConfig(@Nullable Configuration config) { - if(config == null) config = new Configuration(); + public static void saveConfig(@Nullable ConfigContainer config) { + if(config == null) config = new ConfigContainer(); try (FileWriter file = new FileWriter(configFile)) { file.write(gson.toJson(config)); @@ -231,7 +216,7 @@ public final class Grasscutter { } } - public static Configuration getConfig() { + public static ConfigContainer getConfig() { return config; } diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 40b8994ec..9414a89c4 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -16,7 +16,7 @@ public final class ReloadCommand implements CommandHandler { public void execute(Player sender, Player targetPlayer, List<String> args) { CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_start")); - Grasscutter.reloadConfig(); + Grasscutter.loadConfig(); Grasscutter.loadLanguage(); Grasscutter.getGameServer().getGachaManager().load(); Grasscutter.getGameServer().getDropManager().load(); diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java new file mode 100644 index 000000000..a848dff39 --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -0,0 +1,229 @@ +package emu.grasscutter.utils; + +import com.google.gson.JsonObject; +import emu.grasscutter.Grasscutter; + +import java.io.FileReader; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Locale; + +import static emu.grasscutter.Grasscutter.config; + +/** + * *when your JVM fails* + */ +public class ConfigContainer { + private static int version() { + return 1; + } + + /** + * Attempts to update the server's existing configuration to the latest + */ + public static void updateConfig() { + try { // Check if the server is using a legacy config. + JsonObject configObject = Grasscutter.getGsonFactory() + .fromJson(new FileReader(Grasscutter.configFile), JsonObject.class); + if(!configObject.has("version")) { + Grasscutter.getLogger().info("Updating legacy .."); + Grasscutter.saveConfig(null); + } + } catch (Exception ignored) { } + + var existing = config.version; + var latest = version(); + + if(existing == latest) + return; + + // Create a new configuration instance. + ConfigContainer updated = new ConfigContainer(); + // Update all configuration fields. + Field[] fields = ConfigContainer.class.getDeclaredFields(); + Arrays.stream(fields).forEach(field -> { + try { + field.set(updated, field.get(config)); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to update a configuration field.", exception); + } + }); updated.version = version(); + + try { // Save configuration & reload. + Grasscutter.saveConfig(updated); + Grasscutter.loadConfig(); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to inject the updated ", exception); + } + } + + public Structure folderStructure = new Structure(); + public Database databaseInfo = new Database(); + public Language language = new Language(); + public Account account = new Account(); + public Server server = new Server(); + + // DO NOT. TOUCH. THE VERSION NUMBER. + public int version = version(); + + /* Option containers. */ + + public static class Database { + public String connectionUri = "mongodb://localhost:27017"; + public String collection = "grasscutter"; + } + + public static class Structure { + public String resources = "./resources/"; + public String data = "./data/"; + public String packets = "./packets/"; + public String keys = "./keys/"; + public String scripts = "./resources/scripts/"; + public String plugins = "./plugins/"; + + // UNUSED (potentially added later?) + // public String dumps = "./dumps/"; + } + + public static class Server { + public Grasscutter.ServerDebugMode debugLevel = Grasscutter.ServerDebugMode.NONE; + public Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID; + + public Dispatch dispatch = new Dispatch(); + public Game game = new Game(); + } + + public static class Language { + public Locale language = Locale.getDefault(); + public Locale fallback = Locale.US; + } + + public static class Account { + public boolean autoCreate = false; + public String[] defaultPermissions = {}; + } + + /* Server options. */ + + public static class Dispatch { + public String bindAddress = "0.0.0.0"; + /* This is the address used in URLs. */ + public String accessAddress = "127.0.0.1"; + + public int bindPort = 443; + /* This is the port used in URLs. */ + public int accessPort = 443; + + public Encryption encryption = new Encryption(); + public Policies policies = new Policies(); + public Region[] regions = {}; + + public String defaultName = "Grasscutter"; + } + + public static class Game { + public String bindAddress = "0.0.0.0"; + /* This is the address used in the default region. */ + public String accessAddress = "127.0.0.1"; + + public int bindPort = 443; + /* This is the port used in the default region. */ + public int accessPort = 443; + + public GameOptions gameOptions = new GameOptions(); + public JoinOptions joinOptions = new JoinOptions(); + public ConsoleAccount serverAccount = new ConsoleAccount(); + } + + /* Data containers. */ + + public static class Encryption { + public boolean useEncryption = true; + /* Should 'https' be appended to URLs? */ + public boolean useInRouting = true; + public String keystore = "./keystore.p12"; + public String keystorePassword = "123456"; + } + + public static class Policies { + public Policies.CORS cors = new Policies.CORS(); + + public static class CORS { + public boolean enabled = false; + public String[] allowedOrigins = new String[]{"*"}; + } + } + + public static class GameOptions { + public GameOptions.InventoryLimits inventoryLimits = new GameOptions.InventoryLimits(); + public GameOptions.AvatarLimits avatarLimits = new GameOptions.AvatarLimits(); + public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. + + public boolean watchGachaConfig = false; + public boolean enableShopItems = true; + public boolean staminaUsage = true; + public GameOptions.Rates rates = new GameOptions.Rates(); + + public Database databaseInfo = new Database(); + + public static class InventoryLimits { + public int weapons = 2000; + public int relics = 2000; + public int materials = 2000; + public int furniture = 2000; + public int all = 30000; + } + + public static class AvatarLimits { + public int singlePlayerTeam = 4; + public int multiplayerTeam = 4; + } + + public static class Rates { + public float adventureExp = 1.0f; + public float mora = 1.0f; + public float leyLines = 1.0f; + } + } + + public static class JoinOptions { + public int[] welcomeEmotes = {2007, 1002, 4010}; + public String welcomeMessage = "Welcome to a Grasscutter server."; + public JoinOptions.Mail welcomeMail = new JoinOptions.Mail(); + + public static class Mail { + public String title = "Welcome to Grasscutter!"; + public String content = """ + Hi there!\r + First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r + \r + Check out our:\r + <type="browser" text="Discord" href="https://discord.gg/T5vZU6UyeG"/> + """; + public String sender = "Lawnmower"; + public emu.grasscutter.game.mail.Mail.MailItem[] items = { + new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1), + new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1) + }; + } + } + + public static class ConsoleAccount { + public int avatarId = 10000007; + public int nameCardId = 210001; + public int adventureRank = 1; + public int worldLevel = 0; + + public String nickName = "Server"; + public String signature = "Welcome to Grasscutter!"; + } + + /* Objects. */ + + public static class Region { + public String Name = "os_usa"; + public String Title = "Grasscutter"; + public String Ip = "127.0.0.1"; + public int Port = 22102; + } +} diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 6b8365170..4af62bfb4 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -11,7 +11,6 @@ import java.util.Map; import java.util.Random; import java.util.Locale; -import emu.grasscutter.Configuration; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -175,7 +174,7 @@ public final class Utils { * Checks for required files and folders before startup. */ public static void startupCheck() { - Configuration config = Grasscutter.getConfig(); + ConfigContainer config = Grasscutter.getConfig(); Logger logger = Grasscutter.getLogger(); boolean exit = false; From d575b79a0c0767072cc67e9e8663f802297d92af Mon Sep 17 00:00:00 2001 From: gentlespoon <github@gentlespoon.com> Date: Wed, 11 May 2022 00:44:22 -0700 Subject: [PATCH 266/434] Fix incorrect ascension level in givechar command --- .../java/emu/grasscutter/command/commands/GiveCharCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java index 2917c64c4..040d07d59 100644 --- a/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/GiveCharCommand.java @@ -63,9 +63,10 @@ public final class GiveCharCommand implements CommandHandler { // Calculate ascension level. int ascension; if (level <= 40) { - ascension = (int) Math.ceil(level / 20f); + ascension = (int) Math.ceil(level / 20f) - 1; } else { ascension = (int) Math.ceil(level / 10f) - 3; + ascension = Math.min(ascension, 6); } Avatar avatar = new Avatar(avatarId); From 2cd1d32fbe7728bdbdeb17db1bb2c16688d0e949 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Wed, 11 May 2022 00:16:09 -0700 Subject: [PATCH 267/434] Bug fixes. * Fix default port for the game server * Fix the returning region info --- .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 4 ++-- src/main/java/emu/grasscutter/utils/ConfigContainer.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 3ae4ea08a..2ff35a3e2 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -125,8 +125,8 @@ public final class DispatchServer { servers.add(server); RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp(lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress)) - .setGateserverPort(lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)) + .setGateserverIp(lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress)) + .setGateserverPort(lr(GAME_INFO.accessPort, GAME_INFO.bindPort)) .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) .build(); diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index a848dff39..0a453191c 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -126,9 +126,9 @@ public class ConfigContainer { /* This is the address used in the default region. */ public String accessAddress = "127.0.0.1"; - public int bindPort = 443; + public int bindPort = 22102; /* This is the port used in the default region. */ - public int accessPort = 443; + public int accessPort = 22102; public GameOptions gameOptions = new GameOptions(); public JoinOptions joinOptions = new JoinOptions(); From 6be39eafd28d746b6ad10fa69b0a1ae66d11499e Mon Sep 17 00:00:00 2001 From: Secretboy-SMR <secretboy.smr@icloud.com> Date: Wed, 11 May 2022 15:27:05 +0800 Subject: [PATCH 268/434] fixed language not found prompt --- .../emu/grasscutter/command/commands/LanguageCommand.java | 7 +++++++ src/main/java/emu/grasscutter/utils/Language.java | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java index 5966c6167..023b6a088 100644 --- a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java @@ -44,6 +44,13 @@ public final class LanguageCommand implements CommandHandler { actualLangCode = languageInst.getLanguageCode(); Grasscutter.setLanguage(languageInst); } + + if (!langCode.equals(actualLangCode)) { + // I think there is no necessary to register this in language files + // since this will always be english + CommandHandler.sendMessage(sender, "currently, server does not have that language: " + langCode); + } + CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_changed", actualLangCode)); } diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 4698606d7..094f7465f 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -125,23 +125,23 @@ public final class Language { InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); if (file == null) { // Provided fallback language. + Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); actualLanguageCode = fallbackLanguageCode; if (cachedLanguages.containsKey(actualLanguageCode)) { return new LanguageStreamDescription(actualLanguageCode, null); } file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); - Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); } if(file == null) { // Fallback the fallback language. + Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); actualLanguageCode = "en-US"; if (cachedLanguages.containsKey(actualLanguageCode)) { return new LanguageStreamDescription(actualLanguageCode, null); } file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); - Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); } if(file == null) From ffd0212f94b9f0d7e8cb403d2feaed43acd35f04 Mon Sep 17 00:00:00 2001 From: coooookies <1164557342@qq.com> Date: Wed, 11 May 2022 16:54:31 +0800 Subject: [PATCH 269/434] Show server status to three-party game launcher --- .../grasscutter/server/dispatch/DispatchServer.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 2ff35a3e2..df1ce666d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.protobuf.ByteString; +import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerRunMode; @@ -258,6 +259,15 @@ public final class DispatchServer { httpServer.post("/authentication/register", (req, res) -> this.getAuthHandler().handleRegister(req, res)); httpServer.post("/authentication/change_password", (req, res) -> this.getAuthHandler().handleChangePassword(req, res)); + // Server Status + httpServer.get("/status/server", (req, res) -> { + + int playerCount = Grasscutter.getGameServer().getPlayers().size(); + String version = GameConstants.VERSION; + + res.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}"); + }); + // Dispatch httpServer.get("/query_region_list", (req, res) -> { // Log From 98066f5015f978127177ad328045425bb762b85d Mon Sep 17 00:00:00 2001 From: Secretboy-SMR <secretboy.smr@icloud.com> Date: Wed, 11 May 2022 18:06:37 +0800 Subject: [PATCH 270/434] Removed invalid code in getLanguageFileDescription,When the language is not discovered, it will use the built-in language fallback mechanism to fall back,At the same time, we also fix the issue that using language in the server does not save the settings of the server side locale --- .../command/commands/LanguageCommand.java | 16 ++++++++-------- .../java/emu/grasscutter/utils/Language.java | 6 +----- src/main/resources/languages/en-US.json | 1 + src/main/resources/languages/zh-CN.json | 1 + 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java index 023b6a088..4783af0cc 100644 --- a/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/LanguageCommand.java @@ -31,24 +31,24 @@ public final class LanguageCommand implements CommandHandler { } String langCode = args.get(0); - String actualLangCode = null; + + var languageInst = Grasscutter.getLanguage(langCode); + var actualLangCode = languageInst.getLanguageCode(); + var locale = Locale.forLanguageTag(actualLangCode); if (sender != null) { - var locale = Locale.forLanguageTag(langCode); - actualLangCode = Utils.getLanguageCode(locale); var account = sender.getAccount(); account.setLocale(locale); account.save(); } else { - var languageInst = Grasscutter.getLanguage(langCode); - actualLangCode = languageInst.getLanguageCode(); Grasscutter.setLanguage(languageInst); + var config = Grasscutter.getConfig(); + config.language.language = locale; + Grasscutter.saveConfig(config); } if (!langCode.equals(actualLangCode)) { - // I think there is no necessary to register this in language files - // since this will always be english - CommandHandler.sendMessage(sender, "currently, server does not have that language: " + langCode); + CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_not_found", langCode)); } CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_changed", actualLangCode)); diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 094f7465f..3789f594a 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -116,12 +116,8 @@ public final class Language { private static LanguageStreamDescription getLanguageFileDescription(String languageCode, String fallbackLanguageCode) { var fileName = languageCode + ".json"; var fallback = fallbackLanguageCode + ".json"; - - String actualLanguageCode = languageCode; - if (cachedLanguages.containsKey(actualLanguageCode)) { - return new LanguageStreamDescription(actualLanguageCode, null); - } + String actualLanguageCode = languageCode; InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); if (file == null) { // Provided fallback language. diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 893f490af..c9c3c0c70 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -191,6 +191,7 @@ "language": { "current_language": "current language is %s", "language_changed": "language changed to %s", + "language_not_found": "currently, server does not have that language: %s", "description": "display or change current language" }, "list": { diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 8d5b35137..2f9663f4f 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -191,6 +191,7 @@ "language": { "current_language": "当前语言是: %s", "language_changed": "语言切换至: %s", + "language_not_found": "目前服务端没有这种语言: %s", "description": "显示或切换当前语言。" }, "list": { From 285405cee4add88c6cd58cb4767e7cf43f5908e0 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Wed, 11 May 2022 03:56:40 -0700 Subject: [PATCH 271/434] Implement quests --- proto/ChildQuest.proto | 9 + proto/CutSceneBeginNotify.proto | 8 + proto/CutSceneEndNotify.proto | 8 + proto/CutSceneFinishNotify.proto | 7 + proto/FinishedParentQuestNotify.proto | 9 + proto/FinishedParentQuestUpdateNotify.proto | 9 + proto/ParentQuest.proto | 18 ++ proto/ParentQuestRandomInfo.proto | 9 + proto/Quest.proto | 20 ++ proto/QuestDelNotify.proto | 7 + proto/QuestGlobalVar.proto | 8 + proto/QuestGlobalVarNotify.proto | 9 + proto/QuestListNotify.proto | 9 + proto/QuestListUpdateNotify.proto | 9 + proto/QuestProgressUpdateNotify.proto | 9 + proto/QuestUpdateQuestVarNotify.proto | 9 + proto/QuestUpdateQuestVarReq.proto | 12 ++ proto/QuestUpdateQuestVarRsp.proto | 10 + proto/QuestVarOp.proto | 9 + .../ServerCondMeetQuestListUpdateNotify.proto | 8 + .../command/commands/QuestCommand.java | 66 ++++++ .../java/emu/grasscutter/data/GameData.java | 6 + .../emu/grasscutter/data/ResourceLoader.java | 34 +++- .../grasscutter/data/custom/QuestConfig.java | 25 +++ .../data/custom/QuestConfigData.java | 104 ++++++++++ .../grasscutter/database/DatabaseHelper.java | 15 ++ .../grasscutter/database/DatabaseManager.java | 5 +- .../emu/grasscutter/game/player/Player.java | 38 +++- .../grasscutter/game/quest/GameMainQuest.java | 124 ++++++++++++ .../emu/grasscutter/game/quest/GameQuest.java | 188 ++++++++++++++++++ .../grasscutter/game/quest/QuestManager.java | 119 +++++++++++ .../game/quest/enums/LogicType.java | 23 +++ .../game/quest/enums/ParentQuestState.java | 18 ++ .../game/quest/enums/QuestCondType.java | 92 +++++++++ .../game/quest/enums/QuestExecType.java | 82 ++++++++ .../game/quest/enums/QuestGuideType.java | 17 ++ .../game/quest/enums/QuestShowType.java | 16 ++ .../game/quest/enums/QuestState.java | 19 ++ .../game/quest/enums/QuestType.java | 22 ++ .../game/quest/enums/ShowQuestGuideType.java | 17 ++ .../send/PacketFinishedParentQuestNotify.java | 22 ++ ...PacketFinishedParentQuestUpdateNotify.java | 19 ++ .../packet/send/PacketQuestListNotify.java | 23 +++ .../send/PacketQuestListUpdateNotify.java | 20 ++ .../send/PacketQuestProgressUpdateNotify.java | 30 +++ ...etServerCondMeetQuestListUpdateNotify.java | 37 ++++ src/main/resources/languages/en-US.json | 8 + 47 files changed, 1379 insertions(+), 6 deletions(-) create mode 100644 proto/ChildQuest.proto create mode 100644 proto/CutSceneBeginNotify.proto create mode 100644 proto/CutSceneEndNotify.proto create mode 100644 proto/CutSceneFinishNotify.proto create mode 100644 proto/FinishedParentQuestNotify.proto create mode 100644 proto/FinishedParentQuestUpdateNotify.proto create mode 100644 proto/ParentQuest.proto create mode 100644 proto/ParentQuestRandomInfo.proto create mode 100644 proto/Quest.proto create mode 100644 proto/QuestDelNotify.proto create mode 100644 proto/QuestGlobalVar.proto create mode 100644 proto/QuestGlobalVarNotify.proto create mode 100644 proto/QuestListNotify.proto create mode 100644 proto/QuestListUpdateNotify.proto create mode 100644 proto/QuestProgressUpdateNotify.proto create mode 100644 proto/QuestUpdateQuestVarNotify.proto create mode 100644 proto/QuestUpdateQuestVarReq.proto create mode 100644 proto/QuestUpdateQuestVarRsp.proto create mode 100644 proto/QuestVarOp.proto create mode 100644 proto/ServerCondMeetQuestListUpdateNotify.proto create mode 100644 src/main/java/emu/grasscutter/command/commands/QuestCommand.java create mode 100644 src/main/java/emu/grasscutter/data/custom/QuestConfig.java create mode 100644 src/main/java/emu/grasscutter/data/custom/QuestConfigData.java create mode 100644 src/main/java/emu/grasscutter/game/quest/GameMainQuest.java create mode 100644 src/main/java/emu/grasscutter/game/quest/GameQuest.java create mode 100644 src/main/java/emu/grasscutter/game/quest/QuestManager.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/LogicType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/ParentQuestState.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestCondType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestExecType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestGuideType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestShowType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestState.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/QuestType.java create mode 100644 src/main/java/emu/grasscutter/game/quest/enums/ShowQuestGuideType.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketQuestListNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketQuestListUpdateNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketQuestProgressUpdateNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java diff --git a/proto/ChildQuest.proto b/proto/ChildQuest.proto new file mode 100644 index 000000000..fcec288c1 --- /dev/null +++ b/proto/ChildQuest.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message ChildQuest { + uint32 quest_id = 1; + uint32 state = 2; + uint32 quest_config_id = 3; +} diff --git a/proto/CutSceneBeginNotify.proto b/proto/CutSceneBeginNotify.proto new file mode 100644 index 000000000..9a926541c --- /dev/null +++ b/proto/CutSceneBeginNotify.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CutSceneBeginNotify { + uint32 cutscene_id = 1; + bool is_wait_others = 2; +} diff --git a/proto/CutSceneEndNotify.proto b/proto/CutSceneEndNotify.proto new file mode 100644 index 000000000..c3f91a4e1 --- /dev/null +++ b/proto/CutSceneEndNotify.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CutSceneEndNotify { + int32 retcode = 1; + uint32 cutscene_id = 2; +} diff --git a/proto/CutSceneFinishNotify.proto b/proto/CutSceneFinishNotify.proto new file mode 100644 index 000000000..8c42d8536 --- /dev/null +++ b/proto/CutSceneFinishNotify.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CutSceneFinishNotify { + uint32 cutscene_id = 1; +} diff --git a/proto/FinishedParentQuestNotify.proto b/proto/FinishedParentQuestNotify.proto new file mode 100644 index 000000000..834b18a47 --- /dev/null +++ b/proto/FinishedParentQuestNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "ParentQuest.proto"; + +message FinishedParentQuestNotify { + repeated ParentQuest parent_quest_list = 1; +} diff --git a/proto/FinishedParentQuestUpdateNotify.proto b/proto/FinishedParentQuestUpdateNotify.proto new file mode 100644 index 000000000..82565af5c --- /dev/null +++ b/proto/FinishedParentQuestUpdateNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "ParentQuest.proto"; + +message FinishedParentQuestUpdateNotify { + repeated ParentQuest parent_quest_list = 1; +} diff --git a/proto/ParentQuest.proto b/proto/ParentQuest.proto new file mode 100644 index 000000000..477366e6c --- /dev/null +++ b/proto/ParentQuest.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "ParentQuestRandomInfo.proto"; +import "ChildQuest.proto"; + +message ParentQuest { + uint32 parent_quest_id = 1; + repeated ChildQuest child_quest_list = 2; + bool is_finished = 3; + bool is_random = 4; + ParentQuestRandomInfo random_info = 5; + repeated int32 quest_var = 6; + uint32 parent_quest_state = 7; + uint32 quest_var_seq = 8; + map<uint32, uint32> time_var_map = 9; +} diff --git a/proto/ParentQuestRandomInfo.proto b/proto/ParentQuestRandomInfo.proto new file mode 100644 index 000000000..61f04a74f --- /dev/null +++ b/proto/ParentQuestRandomInfo.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message ParentQuestRandomInfo { + uint32 entrance_id = 1; + uint32 template_id = 2; + repeated uint32 factor_list = 3; +} diff --git a/proto/Quest.proto b/proto/Quest.proto new file mode 100644 index 000000000..a8b8e9adc --- /dev/null +++ b/proto/Quest.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message Quest { + uint32 quest_id = 1; + uint32 state = 2; + uint32 start_time = 4; + bool is_random = 5; + uint32 parent_quest_id = 6; + uint32 quest_config_id = 7; + uint32 start_game_time = 8; + uint32 accept_time = 9; + repeated uint32 lacked_npc_list = 10; + repeated uint32 finish_progress_list = 11; + repeated uint32 fail_progress_list = 12; + map<uint32, uint32> lacked_npc_map = 13; + repeated uint32 lacked_place_list = 14; + map<uint32, uint32> lacked_place_map = 15; +} diff --git a/proto/QuestDelNotify.proto b/proto/QuestDelNotify.proto new file mode 100644 index 000000000..0365ec303 --- /dev/null +++ b/proto/QuestDelNotify.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestDelNotify { + uint32 quest_id = 1; +} diff --git a/proto/QuestGlobalVar.proto b/proto/QuestGlobalVar.proto new file mode 100644 index 000000000..c5df4ac9f --- /dev/null +++ b/proto/QuestGlobalVar.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestGlobalVar { + uint32 key = 1; + int32 value = 2; +} diff --git a/proto/QuestGlobalVarNotify.proto b/proto/QuestGlobalVarNotify.proto new file mode 100644 index 000000000..0803f348c --- /dev/null +++ b/proto/QuestGlobalVarNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "QuestGlobalVar.proto"; + +message QuestGlobalVarNotify { + repeated QuestGlobalVar var_list = 1; +} diff --git a/proto/QuestListNotify.proto b/proto/QuestListNotify.proto new file mode 100644 index 000000000..ae40ba1aa --- /dev/null +++ b/proto/QuestListNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "Quest.proto"; + +message QuestListNotify { + repeated Quest quest_list = 1; +} diff --git a/proto/QuestListUpdateNotify.proto b/proto/QuestListUpdateNotify.proto new file mode 100644 index 000000000..5e78079b6 --- /dev/null +++ b/proto/QuestListUpdateNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "Quest.proto"; + +message QuestListUpdateNotify { + repeated Quest quest_list = 1; +} diff --git a/proto/QuestProgressUpdateNotify.proto b/proto/QuestProgressUpdateNotify.proto new file mode 100644 index 000000000..ac3fccd32 --- /dev/null +++ b/proto/QuestProgressUpdateNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestProgressUpdateNotify { + uint32 quest_id = 1; + repeated uint32 finish_progress_list = 2; + repeated uint32 fail_progress_list = 3; +} diff --git a/proto/QuestUpdateQuestVarNotify.proto b/proto/QuestUpdateQuestVarNotify.proto new file mode 100644 index 000000000..ba61ac4c0 --- /dev/null +++ b/proto/QuestUpdateQuestVarNotify.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestUpdateQuestVarNotify { + uint32 parent_quest_id = 1; + repeated int32 quest_var = 2; + uint32 parent_quest_var_seq = 3; +} diff --git a/proto/QuestUpdateQuestVarReq.proto b/proto/QuestUpdateQuestVarReq.proto new file mode 100644 index 000000000..c89e7f0e7 --- /dev/null +++ b/proto/QuestUpdateQuestVarReq.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "QuestVarOp.proto"; + +message QuestUpdateQuestVarReq { + uint32 quest_id = 1; + repeated QuestVarOp quest_var_op_list = 2; + uint32 parent_quest_id = 3; + uint32 parent_quest_var_seq = 4; +} diff --git a/proto/QuestUpdateQuestVarRsp.proto b/proto/QuestUpdateQuestVarRsp.proto new file mode 100644 index 000000000..6f28cfb59 --- /dev/null +++ b/proto/QuestUpdateQuestVarRsp.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestUpdateQuestVarRsp { + int32 retcode = 1; + uint32 quest_id = 2; + uint32 parent_quest_id = 3; + uint32 parent_quest_var_seq = 4; +} diff --git a/proto/QuestVarOp.proto b/proto/QuestVarOp.proto new file mode 100644 index 000000000..51d4411b9 --- /dev/null +++ b/proto/QuestVarOp.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message QuestVarOp { + uint32 index = 1; + int32 value = 2; + bool is_add = 3; +} diff --git a/proto/ServerCondMeetQuestListUpdateNotify.proto b/proto/ServerCondMeetQuestListUpdateNotify.proto new file mode 100644 index 000000000..4326518ae --- /dev/null +++ b/proto/ServerCondMeetQuestListUpdateNotify.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message ServerCondMeetQuestListUpdateNotify { + repeated uint32 add_quest_id_list = 1; + repeated uint32 del_quest_id_list = 2; +} diff --git a/src/main/java/emu/grasscutter/command/commands/QuestCommand.java b/src/main/java/emu/grasscutter/command/commands/QuestCommand.java new file mode 100644 index 000000000..70fae0120 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/QuestCommand.java @@ -0,0 +1,66 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameQuest; + +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "quest", usage = "quest <add|finish> [quest id]", permission = "player.quest", permissionTargeted = "player.quest.others", description = "commands.quest.description") +public final class QuestCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List<String> args) { + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); + return; + } + + if (args.size() != 2) { + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.usage")); + return; + } + + String cmd = args.get(0).toLowerCase(); + int questId; + + try { + questId = Integer.parseInt(args.get(1)); + } catch (Exception e) { + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.invalid_id")); + return; + } + + switch (cmd) { + case "add" -> { + GameQuest quest = sender.getQuestManager().addQuest(questId); + + if (quest != null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.added", questId)); + return; + } + + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found")); + } + case "finish" -> { + GameQuest quest = sender.getQuestManager().getQuestById(questId); + + if (quest == null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found")); + return; + } + + quest.finish(); + + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.finished", questId)); + } + default -> { + CommandHandler.sendMessage(sender, translate(sender, "commands.quest.usage")); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index ac2472192..2b40818e1 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -12,6 +12,7 @@ import emu.grasscutter.data.custom.AbilityEmbryoEntry; import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.QuestConfig; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.*; import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; @@ -27,6 +28,7 @@ public class GameData { private static final Map<String, AbilityModifierEntry> abilityModifiers = new HashMap<>(); private static final Map<String, OpenConfigEntry> openConfigEntries = new HashMap<>(); private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>(); + private static final Int2ObjectMap<QuestConfig> questConfigs = new Int2ObjectOpenHashMap<>(); // ExcelConfigs private static final Int2ObjectMap<PlayerLevelData> playerLevelDataMap = new Int2ObjectOpenHashMap<>(); @@ -122,6 +124,10 @@ public class GameData { return getScenePointEntries().get(sceneId + "_" + pointId); } + public static Int2ObjectMap<QuestConfig> getQuestConfigs() { + return questConfigs; + } + public static Int2ObjectMap<AvatarData> getAvatarDataMap() { return avatarDataMap; } diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index c2708bd63..ae73de8ff 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -24,6 +24,9 @@ import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; +import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.data.custom.QuestConfigData; +import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.game.world.SpawnDataEntry; import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; @@ -57,8 +60,9 @@ public class ResourceLoader { loadResources(); // Process into depots GameDepot.load(); - // Load spawn data + // Load spawn data and quests loadSpawnData(); + loadQuests(); // Load scene points - must be done AFTER resources are loaded loadScenePoints(); // Custom - TODO move this somewhere else @@ -396,6 +400,34 @@ public class ResourceLoader { GameData.getOpenConfigEntries().put(entry.getName(), entry); } } + + private static void loadQuests() { + File folder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Quest/"); + + if (!folder.exists()) { + return; + } + + for (File file : folder.listFiles()) { + QuestConfigData mainQuest = null; + + try (FileReader fileReader = new FileReader(file)) { + mainQuest = Grasscutter.getGsonFactory().fromJson(fileReader, QuestConfigData.class); + } catch (Exception e) { + e.printStackTrace(); + continue; + } + + if (mainQuest.getSubQuests() != null) { + for (SubQuestConfigData subQuest : mainQuest.getSubQuests()) { + QuestConfig quest = new QuestConfig(mainQuest, subQuest); + GameData.getQuestConfigs().put(quest.getId(), quest); + } + } + } + + Grasscutter.getLogger().info("Loaded " + GameData.getQuestConfigs().size() + " Quest Configs"); + } // BinOutput configs diff --git a/src/main/java/emu/grasscutter/data/custom/QuestConfig.java b/src/main/java/emu/grasscutter/data/custom/QuestConfig.java new file mode 100644 index 000000000..8674ff7ab --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/QuestConfig.java @@ -0,0 +1,25 @@ +package emu.grasscutter.data.custom; + +import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; + +public class QuestConfig { + private final QuestConfigData mainQuest; + private final SubQuestConfigData subQuest; + + public QuestConfig(QuestConfigData mainQuest, SubQuestConfigData subQuest) { + this.mainQuest = mainQuest; + this.subQuest = subQuest; + } + + public int getId() { + return subQuest.getSubId(); + } + + public QuestConfigData getMainQuest() { + return mainQuest; + } + + public SubQuestConfigData getSubQuest() { + return subQuest; + } +} diff --git a/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java b/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java new file mode 100644 index 000000000..4ba0ce47c --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java @@ -0,0 +1,104 @@ +package emu.grasscutter.data.custom; + +import emu.grasscutter.game.quest.enums.LogicType; +import emu.grasscutter.game.quest.enums.QuestCondType; +import emu.grasscutter.game.quest.enums.QuestType; + +public class QuestConfigData { + private int id; + private int series; + private QuestType type; + + private long titleTextMapHash; + private int[] suggestTrackMainQuestList; + private int[] rewardIdList; + + private SubQuestConfigData[] subQuests; + + public int getId() { + return id; + } + + public int getSeries() { + return series; + } + + public QuestType getType() { + return type; + } + + public long getTitleTextMapHash() { + return titleTextMapHash; + } + + public int[] getSuggestTrackMainQuestList() { + return suggestTrackMainQuestList; + } + + public int[] getRewardIdList() { + return rewardIdList; + } + + public SubQuestConfigData[] getSubQuests() { + return subQuests; + } + + public class SubQuestConfigData { + private int subId; + private int mainId; + + private LogicType acceptCondComb; + private QuestCondition[] acceptCond; + + private LogicType finishCondComb; + private QuestCondition[] finishCond; + + private LogicType failCondComb; + private QuestCondition[] failCond; + + public int getSubId() { + return subId; + } + + public int getMainId() { + return mainId; + } + + public LogicType getAcceptCondComb() { + return acceptCondComb; + } + + public QuestCondition[] getAcceptCond() { + return acceptCond; + } + + public LogicType getFinishCondComb() { + return finishCondComb; + } + + public QuestCondition[] getFinishCond() { + return finishCond; + } + + public LogicType getFailCondComb() { + return failCondComb; + } + + public QuestCondition[] getFailCond() { + return failCond; + } + } + + public class QuestCondition { + private QuestCondType type; + private int[] param; + + public QuestCondType getType() { + return type; + } + + public int[] getParam() { + return param; + } + } +} diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 8f1de0bb9..fe931bdc3 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -15,6 +15,7 @@ import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; import static com.mongodb.client.model.Filters.eq; @@ -111,6 +112,8 @@ public final class DatabaseHelper { DatabaseManager.getDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); // Delete GameItem.class data DatabaseManager.getDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); + // Delete GameMainQuest.class data + DatabaseManager.getDatabase().getCollection("quests").deleteMany(eq("ownerUid", target.getPlayerUid())); // Delete friendships. // Here, we need to make sure to not only delete the deleted account's friendships, @@ -260,4 +263,16 @@ public final class DatabaseHelper { DeleteResult result = DatabaseManager.getDatastore().delete(mail); return result.wasAcknowledged(); } + + public static List<GameMainQuest> getAllQuests(Player player) { + return DatabaseManager.getDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); + } + + public static void saveQuest(GameMainQuest quest) { + DatabaseManager.getDatastore().save(quest); + } + + public static boolean deleteQuest(GameMainQuest quest) { + return DatabaseManager.getDatastore().delete(quest).wasAcknowledged(); + } } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 90ff17238..12bb444b8 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -20,6 +20,8 @@ import emu.grasscutter.game.gacha.GachaRecord; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; public final class DatabaseManager { @@ -30,7 +32,8 @@ public final class DatabaseManager { private static Datastore dispatchDatastore; private static final Class<?>[] mappedClasses = new Class<?>[] { - DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class, Mail.class + DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, + GachaRecord.class, Mail.class, GameMainQuest.class }; public static Datastore getDatastore() { diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 477f974ea..e8c4cd61b 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -29,6 +29,9 @@ import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.SceneType; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestManager; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.managers.MapMarkManager.*; import emu.grasscutter.game.tower.TowerManager; @@ -91,6 +94,7 @@ public class Player { @Transient private MailHandler mailHandler; @Transient private MessageHandler messageHandler; @Transient private AbilityManager abilityManager; + @Transient private QuestManager questManager; @Transient private SotSManager sotsManager; @@ -145,6 +149,7 @@ public class Player { this.mailHandler = new MailHandler(this); this.towerManager = new TowerManager(this); this.abilityManager = new AbilityManager(this); + this.setQuestManager(new QuestManager(this)); this.pos = new Position(); this.rotation = new Position(); this.properties = new HashMap<>(); @@ -409,6 +414,14 @@ public class Player { return towerManager; } + public QuestManager getQuestManager() { + return questManager; + } + + public void setQuestManager(QuestManager questManager) { + this.questManager = questManager; + } + public PlayerGachaInfo getGachaInfo() { return gachaInfo; } @@ -883,9 +896,7 @@ public class Player { } public void sendPacket(BasePacket packet) { - if (this.hasSentAvatarDataNotify) { - this.getSession().send(packet); - } + this.getSession().send(packet); } public OnlinePlayerInfo getOnlinePlayerInfo() { @@ -1118,7 +1129,23 @@ public class Player { this.getFriendsList().loadFromDatabase(); this.getMailHandler().loadFromDatabase(); + this.getQuestManager().loadFromDatabase(); + + // Quest - Commented out because a problem is caused if you log out while this quest is active + /* + if (getQuestManager().getMainQuestById(351) == null) { + GameQuest quest = getQuestManager().addQuest(35104); + if (quest != null) { + quest.finish(); + } + getQuestManager().addQuest(35101); + + this.setSceneId(3); + this.getPos().set(GameConstants.START_POSITION); + } + */ + // Create world World world = new World(this); world.addPlayer(this); @@ -1138,7 +1165,10 @@ public class Player { session.send(new PacketStoreWeightLimitNotify()); session.send(new PacketPlayerStoreNotify(this)); session.send(new PacketAvatarDataNotify(this)); - + session.send(new PacketFinishedParentQuestNotify(this)); + session.send(new PacketQuestListNotify(this)); + session.send(new PacketServerCondMeetQuestListUpdateNotify(this)); + getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. session.send(new PacketPlayerEnterSceneNotify(this)); // Enter game world diff --git a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java new file mode 100644 index 000000000..1ceda3356 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java @@ -0,0 +1,124 @@ +package emu.grasscutter.game.quest; + +import java.util.HashMap; +import java.util.Map; + +import org.bson.types.ObjectId; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import dev.morphia.annotations.Indexed; +import dev.morphia.annotations.Transient; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.enums.ParentQuestState; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest; +import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest; +import emu.grasscutter.net.proto.QuestOuterClass.Quest; +import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; +import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; +import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +@Entity(value = "quests", useDiscriminator = false) +public class GameMainQuest { + @Id private ObjectId id; + + @Indexed private int ownerUid; + @Transient private Player owner; + + private Map<Integer, GameQuest> childQuests; + + private int parentQuestId; + private int[] questVars; + private ParentQuestState state; + private boolean isFinished; + + @Deprecated // Morphia only. Do not use. + public GameMainQuest() {} + + public GameMainQuest(Player player, int parentQuestId) { + this.owner = player; + this.ownerUid = player.getUid(); + this.parentQuestId = parentQuestId; + this.childQuests = new HashMap<>(); + this.questVars = new int[5]; + this.state = ParentQuestState.PARENT_QUEST_STATE_NONE; + } + + public int getParentQuestId() { + return parentQuestId; + } + + public int getOwnerUid() { + return ownerUid; + } + + public Player getOwner() { + return owner; + } + + public void setOwner(Player player) { + if (player.getUid() != this.getOwnerUid()) return; + this.owner = player; + } + + public Map<Integer, GameQuest> getChildQuests() { + return childQuests; + } + + public GameQuest getChildQuestById(int id) { + return this.getChildQuests().get(id); + } + + public int[] getQuestVars() { + return questVars; + } + + public ParentQuestState getState() { + return state; + } + + public boolean isFinished() { + return isFinished; + } + + public void finish() { + this.isFinished = true; + this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED; + this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this)); + } + + public void save() { + DatabaseHelper.saveQuest(this); + } + + public ParentQuest toProto() { + ParentQuest.Builder proto = ParentQuest.newBuilder() + .setParentQuestId(getParentQuestId()) + .setIsFinished(isFinished()) + .setParentQuestState(getState().getValue()); + + for (GameQuest quest : this.getChildQuests().values()) { + ChildQuest childQuest = ChildQuest.newBuilder() + .setQuestId(quest.getQuestId()) + .setState(quest.getState().getValue()) + .build(); + + proto.addChildQuestList(childQuest); + } + + if (getQuestVars() != null) { + for (int i : getQuestVars()) { + proto.addQuestVar(i); + } + } + + return proto.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java new file mode 100644 index 000000000..53599830c --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -0,0 +1,188 @@ +package emu.grasscutter.game.quest; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; +import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.net.proto.QuestOuterClass.Quest; +import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify; +import emu.grasscutter.utils.Utils; + +@Entity +public class GameQuest { + @Transient private GameMainQuest mainQuest; + @Transient private QuestConfig config; + + private int questId; + private int mainQuestId; + private QuestState state; + + private int startTime; + private int acceptTime; + private int finishTime; + + private int[] finishProgressList; + private int[] failProgressList; + + @Deprecated // Morphia only. Do not use. + public GameQuest() {} + + public GameQuest(GameMainQuest mainQuest, QuestConfig config) { + this.mainQuest = mainQuest; + this.questId = config.getId(); + this.mainQuestId = config.getMainQuest().getId(); + this.config = config; + this.acceptTime = Utils.getCurrentSeconds(); + this.startTime = this.acceptTime; + this.state = QuestState.QUEST_STATE_UNFINISHED; + + if (config.getSubQuest().getFinishCond() != null) { + this.finishProgressList = new int[config.getSubQuest().getFinishCond().length]; + } + + if (config.getSubQuest().getFailCond() != null) { + this.failProgressList = new int[config.getSubQuest().getFailCond().length]; + } + + this.mainQuest.getChildQuests().put(this.questId, this); + } + + public GameMainQuest getMainQuest() { + return mainQuest; + } + + public void setMainQuest(GameMainQuest mainQuest) { + this.mainQuest = mainQuest; + } + + public Player getOwner() { + return getMainQuest().getOwner(); + } + + public int getQuestId() { + return questId; + } + + public int getMainQuestId() { + return mainQuestId; + } + + public QuestConfig getConfig() { + return config; + } + + public void setConfig(QuestConfig config) { + if (this.getQuestId() != config.getId()) return; + this.config = config; + } + + public QuestState getState() { + return state; + } + + public void setState(QuestState state) { + this.state = state; + } + + public int getStartTime() { + return startTime; + } + + public void setStartTime(int startTime) { + this.startTime = startTime; + } + + public int getAcceptTime() { + return acceptTime; + } + + public void setAcceptTime(int acceptTime) { + this.acceptTime = acceptTime; + } + + public int getFinishTime() { + return finishTime; + } + + public void setFinishTime(int finishTime) { + this.finishTime = finishTime; + } + + public int[] getFinishProgressList() { + return finishProgressList; + } + + public void setFinishProgress(int index, int value) { + finishProgressList[index] = value; + } + + public int[] getFailProgressList() { + return failProgressList; + } + + public void setFailProgress(int index, int value) { + failProgressList[index] = value; + } + + public void finish() { + this.state = QuestState.QUEST_STATE_FINISHED; + this.finishTime = Utils.getCurrentSeconds(); + + if (this.getFinishProgressList() != null) { + for (int i = 0 ; i < getFinishProgressList().length; i++) { + getFinishProgressList()[i] = 1; + } + } + + this.getOwner().getSession().send(new PacketQuestProgressUpdateNotify(this)); + + // Finish main quest if all child quests are done + this.tryFinishMainQuest(); + this.save(); + } + + public boolean tryFinishMainQuest() { + try { + SubQuestConfigData subQuestData = getConfig().getMainQuest().getSubQuests()[getConfig().getMainQuest().getSubQuests().length - 1]; + + if (subQuestData.getSubId() == this.getQuestId()) { + getMainQuest().finish(); + return true; + } + } catch (Exception e) { + + } + + return false; + } + + public void save() { + getMainQuest().save(); + } + + public Quest toProto() { + Quest.Builder proto = Quest.newBuilder() + .setQuestId(this.getQuestId()) + .setState(this.getState().getValue()) + .setParentQuestId(this.getMainQuestId()) + .setStartTime(this.getStartTime()) + .setStartGameTime(438) + .setAcceptTime(this.getAcceptTime()); + + if (this.getFinishProgressList() != null) { + for (int i : this.getFinishProgressList()) { + proto.addFinishProgressList(i); + } + } + + if (this.getFailProgressList() != null) { + for (int i : this.getFailProgressList()) { + proto.addFailProgressList(i); + } + } + + return proto.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java new file mode 100644 index 000000000..e1c26704c --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -0,0 +1,119 @@ +package emu.grasscutter.game.quest; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; +import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; +import emu.grasscutter.server.packet.send.PacketServerCondMeetQuestListUpdateNotify; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +public class QuestManager { + private final Player player; + private final Int2ObjectMap<GameMainQuest> quests; + + public QuestManager(Player player) { + this.player = player; + this.quests = new Int2ObjectOpenHashMap<>(); + } + + public Player getPlayer() { + return player; + } + + public Int2ObjectMap<GameMainQuest> getQuests() { + return quests; + } + + public GameMainQuest getMainQuestById(int mainQuestId) { + return getQuests().get(mainQuestId); + } + + public GameQuest getQuestById(int questId) { + QuestConfig questConfig = GameData.getQuestConfigs().get(questId); + if (questConfig == null) { + return null; + } + + GameMainQuest mainQuest = getQuests().get(questConfig.getMainQuest().getId()); + + if (mainQuest == null) { + return null; + } + + return mainQuest.getChildQuests().get(questId); + } + + public void forEachQuest(Consumer<GameQuest> callback) { + for (GameMainQuest mainQuest : getQuests().values()) { + for (GameQuest quest : mainQuest.getChildQuests().values()) { + callback.accept(quest); + } + } + } + + public GameMainQuest addMainQuest(QuestConfig questConfig) { + GameMainQuest mainQuest = new GameMainQuest(getPlayer(), questConfig.getMainQuest().getId()); + getQuests().put(mainQuest.getParentQuestId(), mainQuest); + + getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(mainQuest)); + + return mainQuest; + } + + public GameQuest addQuest(int questId) { + QuestConfig questConfig = GameData.getQuestConfigs().get(questId); + if (questConfig == null) { + return null; + } + + // Main quest + GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainQuest().getId()); + + // Create main quest if it doesnt exist + if (mainQuest == null) { + mainQuest = addMainQuest(questConfig); + } + + // Sub quest + GameQuest quest = mainQuest.getChildQuestById(questId); + + if (quest != null) { + return null; + } + + // Create + quest = new GameQuest(mainQuest, questConfig); + + // Save main quest + mainQuest.save(); + + // Send packet + getPlayer().sendPacket(new PacketServerCondMeetQuestListUpdateNotify(quest)); + getPlayer().sendPacket(new PacketQuestListUpdateNotify(quest)); + + return quest; + } + + public void loadFromDatabase() { + List<GameMainQuest> quests = DatabaseHelper.getAllQuests(getPlayer()); + + for (GameMainQuest mainQuest : quests) { + mainQuest.setOwner(this.getPlayer()); + + for (GameQuest quest : mainQuest.getChildQuests().values()) { + quest.setMainQuest(mainQuest); + quest.setConfig(GameData.getQuestConfigs().get(quest.getQuestId())); + } + + this.getQuests().put(mainQuest.getParentQuestId(), mainQuest); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/LogicType.java b/src/main/java/emu/grasscutter/game/quest/enums/LogicType.java new file mode 100644 index 000000000..608ec9c28 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/LogicType.java @@ -0,0 +1,23 @@ +package emu.grasscutter.game.quest.enums; + +public enum LogicType { + LOGIC_NONE (0), + LOGIC_AND (1), + LOGIC_OR (2), + LOGIC_NOT (3), + LOGIC_A_AND_ETCOR (4), + LOGIC_A_AND_B_AND_ETCOR (5), + LOGIC_A_OR_ETCAND (6), + LOGIC_A_OR_B_OR_ETCAND (7), + LOGIC_A_AND_B_OR_ETCAND (8); + + private final int value; + + LogicType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/ParentQuestState.java b/src/main/java/emu/grasscutter/game/quest/enums/ParentQuestState.java new file mode 100644 index 000000000..6c7805f8d --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/ParentQuestState.java @@ -0,0 +1,18 @@ +package emu.grasscutter.game.quest.enums; + +public enum ParentQuestState { + PARENT_QUEST_STATE_NONE (0), + PARENT_QUEST_STATE_FINISHED (1), + PARENT_QUEST_STATE_FAILED (2), + PARENT_QUEST_STATE_CANCELED (3); + + private final int value; + + ParentQuestState(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestCondType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestCondType.java new file mode 100644 index 000000000..42db14f2d --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestCondType.java @@ -0,0 +1,92 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestCondType { + QUEST_COND_NONE (0), + QUEST_COND_STATE_EQUAL (1), + QUEST_COND_STATE_NOT_EQUAL (2), + QUEST_COND_PACK_HAVE_ITEM (3), + QUEST_COND_AVATAR_ELEMENT_EQUAL (4), + QUEST_COND_AVATAR_ELEMENT_NOT_EQUAL (5), + QUEST_COND_AVATAR_CAN_CHANGE_ELEMENT (6), + QUEST_COND_CITY_LEVEL_EQUAL_GREATER (7), + QUEST_COND_ITEM_NUM_LESS_THAN (8), + QUEST_COND_DAILY_TASK_START (9), + QUEST_COND_OPEN_STATE_EQUAL (10), + QUEST_COND_DAILY_TASK_OPEN (11), + QUEST_COND_DAILY_TASK_REWARD_CAN_GET (12), + QUEST_COND_DAILY_TASK_REWARD_RECEIVED (13), + QUEST_COND_PLAYER_LEVEL_REWARD_CAN_GET (14), + QUEST_COND_EXPLORATION_REWARD_CAN_GET (15), + QUEST_COND_IS_WORLD_OWNER (16), + QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER (17), + QUEST_COND_SCENE_AREA_UNLOCKED (18), + QUEST_COND_ITEM_GIVING_ACTIVED (19), + QUEST_COND_ITEM_GIVING_FINISHED (20), + QUEST_COND_IS_DAYTIME (21), + QUEST_COND_CURRENT_AVATAR (22), + QUEST_COND_CURRENT_AREA (23), + QUEST_COND_QUEST_VAR_EQUAL (24), + QUEST_COND_QUEST_VAR_GREATER (25), + QUEST_COND_QUEST_VAR_LESS (26), + QUEST_COND_FORGE_HAVE_FINISH (27), + QUEST_COND_DAILY_TASK_IN_PROGRESS (28), + QUEST_COND_DAILY_TASK_FINISHED (29), + QUEST_COND_ACTIVITY_COND (30), + QUEST_COND_ACTIVITY_OPEN (31), + QUEST_COND_DAILY_TASK_VAR_GT (32), + QUEST_COND_DAILY_TASK_VAR_EQ (33), + QUEST_COND_DAILY_TASK_VAR_LT (34), + QUEST_COND_BARGAIN_ITEM_GT (35), + QUEST_COND_BARGAIN_ITEM_EQ (36), + QUEST_COND_BARGAIN_ITEM_LT (37), + QUEST_COND_COMPLETE_TALK (38), + QUEST_COND_NOT_HAVE_BLOSSOM_TALK (39), + QUEST_COND_IS_CUR_BLOSSOM_TALK (40), + QUEST_COND_QUEST_NOT_RECEIVE (41), + QUEST_COND_QUEST_SERVER_COND_VALID (42), + QUEST_COND_ACTIVITY_CLIENT_COND (43), + QUEST_COND_QUEST_GLOBAL_VAR_EQUAL (44), + QUEST_COND_QUEST_GLOBAL_VAR_GREATER (45), + QUEST_COND_QUEST_GLOBAL_VAR_LESS (46), + QUEST_COND_PERSONAL_LINE_UNLOCK (47), + QUEST_COND_CITY_REPUTATION_REQUEST (48), + QUEST_COND_MAIN_COOP_START (49), + QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT (50), + QUEST_COND_CITY_REPUTATION_LEVEL (51), + QUEST_COND_CITY_REPUTATION_UNLOCK (52), + QUEST_COND_LUA_NOTIFY (53), + QUEST_COND_CUR_CLIMATE (54), + QUEST_COND_ACTIVITY_END (55), + QUEST_COND_COOP_POINT_RUNNING (56), + QUEST_COND_GADGET_TALK_STATE_EQUAL (57), + QUEST_COND_AVATAR_FETTER_GT (58), + QUEST_COND_AVATAR_FETTER_EQ (59), + QUEST_COND_AVATAR_FETTER_LT (60), + QUEST_COND_NEW_HOMEWORLD_MOUDLE_UNLOCK (61), + QUEST_COND_NEW_HOMEWORLD_LEVEL_REWARD (62), + QUEST_COND_NEW_HOMEWORLD_MAKE_FINISH (63), + QUEST_COND_HOMEWORLD_NPC_EVENT (64), + QUEST_COND_TIME_VAR_GT_EQ (65), + QUEST_COND_TIME_VAR_PASS_DAY (66), + QUEST_COND_HOMEWORLD_NPC_NEW_TALK (67), + QUEST_COND_PLAYER_CHOOSE_MALE (68), + QUEST_COND_HISTORY_GOT_ANY_ITEM (69), + QUEST_COND_LEARNED_RECIPE (70), + QUEST_COND_LUNARITE_REGION_UNLOCKED (71), + QUEST_COND_LUNARITE_HAS_REGION_HINT_COUNT (72), + QUEST_COND_LUNARITE_COLLECT_FINISH (73), + QUEST_COND_LUNARITE_MARK_ALL_FINISH (74), + QUEST_COND_NEW_HOMEWORLD_SHOP_ITEM (75), + QUEST_COND_SCENE_POINT_UNLOCK (76), + QUEST_COND_SCENE_LEVEL_TAG_EQ (77); + + private final int value; + + QuestCondType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestExecType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestExecType.java new file mode 100644 index 000000000..4f3c2557c --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestExecType.java @@ -0,0 +1,82 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestExecType { + QUEST_EXEC_NONE (0), + QUEST_EXEC_DEL_PACK_ITEM (1), + QUEST_EXEC_UNLOCK_POINT (2), + QUEST_EXEC_UNLOCK_AREA (3), + QUEST_EXEC_UNLOCK_FORCE (4), + QUEST_EXEC_LOCK_FORCE (5), + QUEST_EXEC_CHANGE_AVATAR_ELEMET (6), + QUEST_EXEC_REFRESH_GROUP_MONSTER (7), + QUEST_EXEC_SET_IS_FLYABLE (8), + QUEST_EXEC_SET_IS_WEATHER_LOCKED (9), + QUEST_EXEC_SET_IS_GAME_TIME_LOCKED (10), + QUEST_EXEC_SET_IS_TRANSFERABLE (11), + QUEST_EXEC_GRANT_TRIAL_AVATAR (12), + QUEST_EXEC_OPEN_BORED (13), + QUEST_EXEC_ROLLBACK_QUEST (14), + QUEST_EXEC_NOTIFY_GROUP_LUA (15), + QUEST_EXEC_SET_OPEN_STATE (16), + QUEST_EXEC_LOCK_POINT (17), + QUEST_EXEC_DEL_PACK_ITEM_BATCH (18), + QUEST_EXEC_REFRESH_GROUP_SUITE (19), + QUEST_EXEC_REMOVE_TRIAL_AVATAR (20), + QUEST_EXEC_SET_GAME_TIME (21), + QUEST_EXEC_SET_WEATHER_GADGET (22), + QUEST_EXEC_ADD_QUEST_PROGRESS (23), + QUEST_EXEC_NOTIFY_DAILY_TASK (24), + QUEST_EXEC_CREATE_PATTERN_GROUP (25), + QUEST_EXEC_REMOVE_PATTERN_GROUP (26), + QUEST_EXEC_REFRESH_GROUP_SUITE_RANDOM (27), + QUEST_EXEC_ACTIVE_ITEM_GIVING (28), + QUEST_EXEC_DEL_ALL_SPECIFIC_PACK_ITEM (29), + QUEST_EXEC_ROLLBACK_PARENT_QUEST (30), + QUEST_EXEC_LOCK_AVATAR_TEAM (31), + QUEST_EXEC_UNLOCK_AVATAR_TEAM (32), + QUEST_EXEC_UPDATE_PARENT_QUEST_REWARD_INDEX (33), + QUEST_EXEC_SET_DAILY_TASK_VAR (34), + QUEST_EXEC_INC_DAILY_TASK_VAR (35), + QUEST_EXEC_DEC_DAILY_TASK_VAR (36), + QUEST_EXEC_ACTIVE_ACTIVITY_COND_STATE (37), + QUEST_EXEC_INACTIVE_ACTIVITY_COND_STATE (38), + QUEST_EXEC_ADD_CUR_AVATAR_ENERGY (39), + QUEST_EXEC_START_BARGAIN (41), + QUEST_EXEC_STOP_BARGAIN (42), + QUEST_EXEC_SET_QUEST_GLOBAL_VAR (43), + QUEST_EXEC_INC_QUEST_GLOBAL_VAR (44), + QUEST_EXEC_DEC_QUEST_GLOBAL_VAR (45), + QUEST_EXEC_REGISTER_DYNAMIC_GROUP (46), + QUEST_EXEC_UNREGISTER_DYNAMIC_GROUP (47), + QUEST_EXEC_SET_QUEST_VAR (48), + QUEST_EXEC_INC_QUEST_VAR (49), + QUEST_EXEC_DEC_QUEST_VAR (50), + QUEST_EXEC_RANDOM_QUEST_VAR (51), + QUEST_EXEC_ACTIVATE_SCANNING_PIC (52), + QUEST_EXEC_RELOAD_SCENE_TAG (53), + QUEST_EXEC_REGISTER_DYNAMIC_GROUP_ONLY (54), + QUEST_EXEC_CHANGE_SKILL_DEPOT (55), + QUEST_EXEC_ADD_SCENE_TAG (56), + QUEST_EXEC_DEL_SCENE_TAG (57), + QUEST_EXEC_INIT_TIME_VAR (58), + QUEST_EXEC_CLEAR_TIME_VAR (59), + QUEST_EXEC_MODIFY_CLIMATE_AREA (60), + QUEST_EXEC_GRANT_TRIAL_AVATAR_AND_LOCK_TEAM (61), + QUEST_EXEC_CHANGE_MAP_AREA_STATE (62), + QUEST_EXEC_DEACTIVE_ITEM_GIVING (63), + QUEST_EXEC_CHANGE_SCENE_LEVEL_TAG (64), + QUEST_EXEC_UNLOCK_PLAYER_WORLD_SCENE (65), + QUEST_EXEC_LOCK_PLAYER_WORLD_SCENE (66), + QUEST_EXEC_FAIL_MAINCOOP (67), + QUEST_EXEC_MODIFY_WEATHER_AREA (68); + + private final int value; + + QuestExecType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestGuideType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestGuideType.java new file mode 100644 index 000000000..45915c6b7 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestGuideType.java @@ -0,0 +1,17 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestGuideType { + QUEST_GUIDE_NONE (0), + QUEST_GUIDE_LOCATION (1), + QUEST_GUIDE_NPC (2); + + private final int value; + + QuestGuideType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestShowType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestShowType.java new file mode 100644 index 000000000..014c1ee06 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestShowType.java @@ -0,0 +1,16 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestShowType { + QUEST_SHOW (0), + QUEST_HIDDEN (1); + + private final int value; + + QuestShowType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestState.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestState.java new file mode 100644 index 000000000..d258a2582 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestState.java @@ -0,0 +1,19 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestState { + QUEST_STATE_NONE (0), + QUEST_STATE_UNSTARTED (1), + QUEST_STATE_UNFINISHED (2), + QUEST_STATE_FINISHED (3), + QUEST_STATE_FAILED (4); + + private final int value; + + QuestState(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestType.java new file mode 100644 index 000000000..fbbac2ae0 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestType.java @@ -0,0 +1,22 @@ +package emu.grasscutter.game.quest.enums; + +public enum QuestType { + AQ (0), + FQ (1), + LQ (2), + EQ (3), + DQ (4), + IQ (5), + VQ (6), + WQ (7); + + private final int value; + + QuestType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/enums/ShowQuestGuideType.java b/src/main/java/emu/grasscutter/game/quest/enums/ShowQuestGuideType.java new file mode 100644 index 000000000..d4e985592 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/enums/ShowQuestGuideType.java @@ -0,0 +1,17 @@ +package emu.grasscutter.game.quest.enums; + +public enum ShowQuestGuideType { + QUEST_GUIDE_ITEM_ENABLE (0), + QUEST_GUIDE_ITEM_DISABLE (1), + QUEST_GUIDE_ITEM_MOVE_HIDE (2); + + private final int value; + + ShowQuestGuideType(int id) { + this.value = id; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java new file mode 100644 index 000000000..7d64da48f --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.FinishedParentQuestNotifyOuterClass.FinishedParentQuestNotify; + +public class PacketFinishedParentQuestNotify extends BasePacket { + + public PacketFinishedParentQuestNotify(Player player) { + super(PacketOpcodes.FinishedParentQuestNotify, true); + + FinishedParentQuestNotify.Builder proto = FinishedParentQuestNotify.newBuilder(); + + for (GameMainQuest mainQuest : player.getQuestManager().getQuests().values()) { + proto.addParentQuestList(mainQuest.toProto()); + } + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java new file mode 100644 index 000000000..68eab7222 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.FinishedParentQuestUpdateNotifyOuterClass.FinishedParentQuestUpdateNotify; + +public class PacketFinishedParentQuestUpdateNotify extends BasePacket { + + public PacketFinishedParentQuestUpdateNotify(GameMainQuest quest) { + super(PacketOpcodes.FinishedParentQuestUpdateNotify); + + FinishedParentQuestUpdateNotify proto = FinishedParentQuestUpdateNotify.newBuilder() + .addParentQuestList(quest.toProto()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListNotify.java new file mode 100644 index 000000000..ccf0d765a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListNotify.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.QuestManager; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.QuestListNotifyOuterClass.QuestListNotify; + +public class PacketQuestListNotify extends BasePacket { + + public PacketQuestListNotify(Player player) { + super(PacketOpcodes.QuestListNotify, true); + + QuestListNotify.Builder proto = QuestListNotify.newBuilder(); + + player.getQuestManager().forEachQuest(quest -> { + proto.addQuestList(quest.toProto()); + }); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListUpdateNotify.java new file mode 100644 index 000000000..adc0767a8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestListUpdateNotify.java @@ -0,0 +1,20 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.QuestListUpdateNotifyOuterClass.QuestListUpdateNotify; + +public class PacketQuestListUpdateNotify extends BasePacket { + + public PacketQuestListUpdateNotify(GameQuest quest) { + super(PacketOpcodes.QuestListUpdateNotify); + + QuestListUpdateNotify proto = QuestListUpdateNotify.newBuilder() + .addQuestList(quest.toProto()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestProgressUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestProgressUpdateNotify.java new file mode 100644 index 000000000..76ee56316 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestProgressUpdateNotify.java @@ -0,0 +1,30 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.QuestProgressUpdateNotifyOuterClass.QuestProgressUpdateNotify; + +public class PacketQuestProgressUpdateNotify extends BasePacket { + + public PacketQuestProgressUpdateNotify(GameQuest quest) { + super(PacketOpcodes.QuestProgressUpdateNotify); + + QuestProgressUpdateNotify.Builder proto = QuestProgressUpdateNotify.newBuilder().setQuestId(quest.getQuestId()); + + if (quest.getFinishProgressList() != null) { + for (int i : quest.getFinishProgressList()) { + proto.addFinishProgressList(i); + } + } + + if (quest.getFailProgressList() != null) { + for (int i : quest.getFailProgressList()) { + proto.addFailProgressList(i); + } + } + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java new file mode 100644 index 000000000..b2ea3d577 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java @@ -0,0 +1,37 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ServerCondMeetQuestListUpdateNotifyOuterClass.ServerCondMeetQuestListUpdateNotify; + +public class PacketServerCondMeetQuestListUpdateNotify extends BasePacket { + + public PacketServerCondMeetQuestListUpdateNotify(Player player) { + super(PacketOpcodes.ServerCondMeetQuestListUpdateNotify); + + ServerCondMeetQuestListUpdateNotify.Builder proto = ServerCondMeetQuestListUpdateNotify.newBuilder(); + + player.getQuestManager().forEachQuest(quest -> { + if (quest.getState().getValue() <= 2) { + proto.addAddQuestIdList(quest.getQuestId()); + } + }); + + this.setData(proto); + } + + public PacketServerCondMeetQuestListUpdateNotify(GameQuest quest) { + super(PacketOpcodes.ServerCondMeetQuestListUpdateNotify); + + ServerCondMeetQuestListUpdateNotify proto = ServerCondMeetQuestListUpdateNotify.newBuilder() + .addAddQuestIdList(quest.getQuestId()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 893f490af..107a12d71 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -210,6 +210,14 @@ "success": "Coordinates: %s, %s, %s\nScene id: %s", "description": "Get coordinates." }, + "quest": { + "description": "Add or finish quests", + "usage": "quest <add|finish> [quest id]", + "added": "Quest %s added", + "finished": "Finished quest %s", + "not_found": "Quest not found", + "invalid_id": "Invalid quest id" + }, "reload": { "reload_start": "Reloading config.", "reload_done": "Reload complete.", From 26575561d6495acf393423cf902bce520e50589c Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Wed, 11 May 2022 04:01:38 -0700 Subject: [PATCH 272/434] Fix build error from merge --- src/main/java/emu/grasscutter/data/ResourceLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 92fda7bb1..5c2ac1ee6 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -400,7 +400,7 @@ public class ResourceLoader { } private static void loadQuests() { - File folder = new File(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Quest/"); + File folder = new File(RESOURCE("BinOutput/Quest/")); if (!folder.exists()) { return; From 3f1128356ee9e167739e7320f08fe11170546a0d Mon Sep 17 00:00:00 2001 From: ayy lmao <ridvan-nuri@windowslive.com> Date: Wed, 11 May 2022 17:35:52 +0300 Subject: [PATCH 273/434] Fix InRouting on dispatch server --- .../java/emu/grasscutter/server/dispatch/DispatchServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index df1ce666d..4e09f8881 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -117,7 +117,7 @@ public final class DispatchServer { .setTitle(DISPATCH_INFO.defaultName) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" + "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + defaultServerName) @@ -150,7 +150,7 @@ public final class DispatchServer { .setTitle(regionInfo.Title) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" + "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + regionInfo.Name) From 89bd4b9aeb8bf5a230605becc673a084706a4218 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 11:38:30 -0400 Subject: [PATCH 274/434] Refactor config database settings --- .../grasscutter/database/DatabaseHelper.java | 68 +++++++++---------- .../grasscutter/database/DatabaseManager.java | 34 +++++----- .../emu/grasscutter/plugin/PluginManager.java | 3 + .../grasscutter/utils/ConfigContainer.java | 29 ++++---- 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index 8f1de0bb9..5b622aac8 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -77,25 +77,25 @@ public final class DatabaseHelper { } public static Account getAccountByName(String username) { - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).first(); + return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("username", username)).first(); } public static Account getAccountByToken(String token) { if(token == null) return null; - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("token", token)).first(); + return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("token", token)).first(); } public static Account getAccountBySessionKey(String sessionKey) { if(sessionKey == null) return null; - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first(); + return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first(); } public static Account getAccountById(String uid) { - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first(); + return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first(); } public static Account getAccountByPlayerId(int playerId) { - return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first(); + return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first(); } public static void deleteAccount(Account target) { @@ -104,37 +104,37 @@ public final class DatabaseHelper { // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... // Delete Mail.class data - DatabaseManager.getDatabase().getCollection("mail").deleteMany(eq("ownerUid", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", target.getPlayerUid())); // Delete Avatar.class data - DatabaseManager.getDatabase().getCollection("avatars").deleteMany(eq("ownerId", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", target.getPlayerUid())); // Delete GachaRecord.class data - DatabaseManager.getDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); // Delete GameItem.class data - DatabaseManager.getDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); // Delete friendships. // Here, we need to make sure to not only delete the deleted account's friendships, // but also all friendship entries for that account's friends. - DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("ownerId", target.getPlayerUid())); - DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("friendId", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("ownerId", target.getPlayerUid())); + DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("friendId", target.getPlayerUid())); // Delete the player. - DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete(); + DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete(); // Finally, delete the account itself. - DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("id", target.getId())).delete(); + DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("id", target.getId())).delete(); } public static List<Player> getAllPlayers() { - return DatabaseManager.getDatastore().find(Player.class).stream().toList(); + return DatabaseManager.getGameDatastore().find(Player.class).stream().toList(); } public static Player getPlayerById(int id) { - return DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("_id", id)).first(); + return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first(); } public static boolean checkPlayerExists(int id) { - return DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("_id", id)).first() != null; + return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first() != null; } public static synchronized Player createPlayer(Player character, int reservedId) { @@ -151,7 +151,7 @@ public final class DatabaseHelper { character.setUid(id); } // Save to database - DatabaseManager.getDatastore().save(character); + DatabaseManager.getGameDatastore().save(character); return character; } @@ -170,48 +170,48 @@ public final class DatabaseHelper { } public static void savePlayer(Player character) { - DatabaseManager.getDatastore().save(character); + DatabaseManager.getGameDatastore().save(character); } public static void saveAvatar(Avatar avatar) { - DatabaseManager.getDatastore().save(avatar); + DatabaseManager.getGameDatastore().save(avatar); } public static List<Avatar> getAvatars(Player player) { - return DatabaseManager.getDatastore().find(Avatar.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); + return DatabaseManager.getGameDatastore().find(Avatar.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); } public static void saveItem(GameItem item) { - DatabaseManager.getDatastore().save(item); + DatabaseManager.getGameDatastore().save(item); } public static boolean deleteItem(GameItem item) { - DeleteResult result = DatabaseManager.getDatastore().delete(item); + DeleteResult result = DatabaseManager.getGameDatastore().delete(item); return result.wasAcknowledged(); } public static List<GameItem> getInventoryItems(Player player) { - return DatabaseManager.getDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); + return DatabaseManager.getGameDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); } public static List<Friendship> getFriends(Player player) { - return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); + return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); } public static List<Friendship> getReverseFriends(Player player) { - return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("friendId", player.getUid())).stream().toList(); + return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("friendId", player.getUid())).stream().toList(); } public static void saveFriendship(Friendship friendship) { - DatabaseManager.getDatastore().save(friendship); + DatabaseManager.getGameDatastore().save(friendship); } public static void deleteFriendship(Friendship friendship) { - DatabaseManager.getDatastore().delete(friendship); + DatabaseManager.getGameDatastore().delete(friendship); } public static Friendship getReverseFriendship(Friendship friendship) { - return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.and( + return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.and( Filters.eq("ownerId", friendship.getFriendId()), Filters.eq("friendId", friendship.getOwnerId()) )).first(); @@ -222,7 +222,7 @@ public final class DatabaseHelper { } public static List<GachaRecord> getGachaRecords(int ownerId, int page, int gachaType, int pageSize){ - return DatabaseManager.getDatastore().find(GachaRecord.class).filter( + return DatabaseManager.getGameDatastore().find(GachaRecord.class).filter( Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType) ).iterator(new FindOptions() @@ -237,7 +237,7 @@ public final class DatabaseHelper { } public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize){ - long count = DatabaseManager.getDatastore().find(GachaRecord.class).filter( + long count = DatabaseManager.getGameDatastore().find(GachaRecord.class).filter( Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType) ).count(); @@ -245,19 +245,19 @@ public final class DatabaseHelper { } public static void saveGachaRecord(GachaRecord gachaRecord){ - DatabaseManager.getDatastore().save(gachaRecord); + DatabaseManager.getGameDatastore().save(gachaRecord); } public static List<Mail> getAllMail(Player player) { - return DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); + return DatabaseManager.getGameDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); } public static void saveMail(Mail mail) { - DatabaseManager.getDatastore().save(mail); + DatabaseManager.getGameDatastore().save(mail); } public static boolean deleteMail(Mail mail) { - DeleteResult result = DatabaseManager.getDatastore().delete(mail); + DeleteResult result = DatabaseManager.getGameDatastore().delete(mail); return result.wasAcknowledged(); } } diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java index 37bda042b..37ec0094d 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseManager.java +++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java @@ -23,19 +23,19 @@ import emu.grasscutter.game.player.Player; import static emu.grasscutter.Configuration.*; public final class DatabaseManager { - private static Datastore datastore; + private static Datastore gameDatastore; private static Datastore dispatchDatastore; private static final Class<?>[] mappedClasses = new Class<?>[] { DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class, Mail.class }; - public static Datastore getDatastore() { - return datastore; + public static Datastore getGameDatastore() { + return gameDatastore; } - public static MongoDatabase getDatabase() { - return getDatastore().getDatabase(); + public static MongoDatabase getGameDatabase() { + return getGameDatastore().getDatabase(); } // Yes. I very dislike this method. However, this will be good for now. @@ -44,42 +44,42 @@ public final class DatabaseManager { if(SERVER.runMode == ServerRunMode.GAME_ONLY) { return dispatchDatastore; } else { - return datastore; + return gameDatastore; } } public static void initialize() { // Initialize - MongoClient mongoClient = MongoClients.create(DATABASE.connectionUri); + MongoClient gameMongoClient = MongoClients.create(DATABASE.game.connectionUri); // Set mapper options. MapperOptions mapperOptions = MapperOptions.builder() .storeEmpties(true).storeNulls(false).build(); // Create data store. - datastore = Morphia.createDatastore(mongoClient, DATABASE.collection, mapperOptions); + gameDatastore = Morphia.createDatastore(gameMongoClient, DATABASE.game.collection, mapperOptions); // Map classes. - datastore.getMapper().map(mappedClasses); + gameDatastore.getMapper().map(mappedClasses); // Ensure indexes try { - datastore.ensureIndexes(); + gameDatastore.ensureIndexes(); } catch (MongoCommandException exception) { Grasscutter.getLogger().info("Mongo index error: ", exception); // Duplicate index error if (exception.getCode() == 85) { // Drop all indexes and re add them - MongoIterable<String> collections = datastore.getDatabase().listCollectionNames(); + MongoIterable<String> collections = gameDatastore.getDatabase().listCollectionNames(); for (String name : collections) { - datastore.getDatabase().getCollection(name).dropIndexes(); + gameDatastore.getDatabase().getCollection(name).dropIndexes(); } // Add back indexes - datastore.ensureIndexes(); + gameDatastore.ensureIndexes(); } } if(SERVER.runMode == ServerRunMode.GAME_ONLY) { - MongoClient dispatchMongoClient = MongoClients.create(GAME_OPTIONS.databaseInfo.connectionUri); - dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, GAME_OPTIONS.databaseInfo.collection); + MongoClient dispatchMongoClient = MongoClients.create(DATABASE.server.connectionUri); + dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, DATABASE.server.collection); // Ensure indexes for dispatch server try { @@ -101,14 +101,14 @@ public final class DatabaseManager { } public static synchronized int getNextId(Class<?> c) { - DatabaseCounter counter = getDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first(); + DatabaseCounter counter = getGameDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first(); if (counter == null) { counter = new DatabaseCounter(c.getSimpleName()); } try { return counter.getNextId(); } finally { - getDatastore().save(counter); + getGameDatastore().save(counter); } } diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java index 5974cac44..5d58744a4 100644 --- a/src/main/java/emu/grasscutter/plugin/PluginManager.java +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -7,6 +7,7 @@ import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.utils.Utils; import java.io.File; +import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.net.MalformedURLException; @@ -90,6 +91,8 @@ public final class PluginManager { fileReader.close(); // Close the file reader. } catch (ClassNotFoundException ignored) { Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); + } catch (FileNotFoundException ignored) { + Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " lacks a valid config file."); } } catch (Exception exception) { Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception); diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 0a453191c..76556700c 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -2,6 +2,8 @@ package emu.grasscutter.utils; import com.google.gson.JsonObject; import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import emu.grasscutter.Grasscutter.ServerRunMode; import java.io.FileReader; import java.lang.reflect.Field; @@ -15,7 +17,7 @@ import static emu.grasscutter.Grasscutter.config; */ public class ConfigContainer { private static int version() { - return 1; + return 2; } /** @@ -69,8 +71,13 @@ public class ConfigContainer { /* Option containers. */ public static class Database { - public String connectionUri = "mongodb://localhost:27017"; - public String collection = "grasscutter"; + public DataStore server = new DataStore(); + public DataStore game = new DataStore(); + + public static class DataStore { + public String connectionUri = "mongodb://localhost:27017"; + public String collection = "grasscutter"; + } } public static class Structure { @@ -86,8 +93,8 @@ public class ConfigContainer { } public static class Server { - public Grasscutter.ServerDebugMode debugLevel = Grasscutter.ServerDebugMode.NONE; - public Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID; + public ServerDebugMode debugLevel = ServerDebugMode.NONE; + public ServerRunMode runMode = ServerRunMode.HYBRID; public Dispatch dispatch = new Dispatch(); public Game game = new Game(); @@ -112,7 +119,7 @@ public class ConfigContainer { public int bindPort = 443; /* This is the port used in URLs. */ - public int accessPort = 443; + public int accessPort = 0; public Encryption encryption = new Encryption(); public Policies policies = new Policies(); @@ -128,7 +135,7 @@ public class ConfigContainer { public int bindPort = 22102; /* This is the port used in the default region. */ - public int accessPort = 22102; + public int accessPort = 0; public GameOptions gameOptions = new GameOptions(); public JoinOptions joinOptions = new JoinOptions(); @@ -155,16 +162,14 @@ public class ConfigContainer { } public static class GameOptions { - public GameOptions.InventoryLimits inventoryLimits = new GameOptions.InventoryLimits(); - public GameOptions.AvatarLimits avatarLimits = new GameOptions.AvatarLimits(); + public InventoryLimits inventoryLimits = new InventoryLimits(); + public AvatarLimits avatarLimits = new AvatarLimits(); public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. public boolean watchGachaConfig = false; public boolean enableShopItems = true; public boolean staminaUsage = true; - public GameOptions.Rates rates = new GameOptions.Rates(); - - public Database databaseInfo = new Database(); + public Rates rates = new Rates(); public static class InventoryLimits { public int weapons = 2000; From 68e6de8ebbfac40de97ef4e59c76f4ad9338b259 Mon Sep 17 00:00:00 2001 From: KingRainbow44 <kobedo11@gmail.com> Date: Wed, 11 May 2022 11:46:36 -0400 Subject: [PATCH 275/434] Add plugin loggers --- src/main/java/emu/grasscutter/plugin/Plugin.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/emu/grasscutter/plugin/Plugin.java b/src/main/java/emu/grasscutter/plugin/Plugin.java index f1ce18a6b..b45e642a5 100644 --- a/src/main/java/emu/grasscutter/plugin/Plugin.java +++ b/src/main/java/emu/grasscutter/plugin/Plugin.java @@ -3,6 +3,8 @@ package emu.grasscutter.plugin; import emu.grasscutter.Grasscutter; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.server.game.GameServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.InputStream; @@ -19,6 +21,7 @@ public abstract class Plugin { private PluginIdentifier identifier; private URLClassLoader classLoader; private File dataFolder; + private Logger logger; /** * This method is reflected into. @@ -35,6 +38,7 @@ public abstract class Plugin { this.identifier = identifier; this.classLoader = classLoader; this.dataFolder = new File(PLUGINS_FOLDER, identifier.name); + this.logger = LoggerFactory.getLogger(identifier.name); if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) { Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name); @@ -103,6 +107,14 @@ public abstract class Plugin { public final ServerHook getHandle() { return this.server; } + + /** + * Returns the plugin's logger. + * @return A SLF4J logger. + */ + public final Logger getLogger() { + return this.logger; + } /* Called when the plugin is first loaded. */ public void onLoad() { } From 1f7f13ffe178cf0d2992446f875c457606298000 Mon Sep 17 00:00:00 2001 From: Secretboy-SMR <secretboy.smr@icloud.com> Date: Wed, 11 May 2022 21:14:07 +0800 Subject: [PATCH 276/434] It will use the english as default rather than tell you the value is not exist if there's no translation for currently language --- src/main/java/emu/grasscutter/utils/Language.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 3789f594a..c343e949e 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -160,7 +160,9 @@ public final class Language { JsonObject object = this.languageData; int index = 0; - String result = "This value does not exist. Please report this to the Discord: " + key; + String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: "; + String result = valueNotFoundPattern + key; + boolean isValueFound = false; while (true) { if(index == keys.length) break; @@ -171,10 +173,18 @@ public final class Language { if(element.isJsonObject()) object = element.getAsJsonObject(); else { + isValueFound = true; result = element.getAsString(); break; } } else break; } + + if (!isValueFound && !languageCode.equals("en-US")) { + var englishValue = Grasscutter.getLanguage("en-US").get(key); + if (!englishValue.contains(valueNotFoundPattern)) { + result += "\nhere is english version:\n" + englishValue; + } + } this.cachedTranslations.put(key, result); return result; } From 1da384091a1093a6c6b1c47a5aff22de37705585 Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon <benjamin7006@gmail.com> Date: Wed, 11 May 2022 20:35:27 +0800 Subject: [PATCH 277/434] Add verifyUser to AuthenticationHandler --- .../dispatch/authentication/AuthenticationHandler.java | 7 +++++++ .../authentication/DefaultAuthenticationHandler.java | 6 ++++++ src/main/resources/languages/en-US.json | 3 +++ 3 files changed, 16 insertions(+) diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java index 92a2961ea..e644a9f1d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java @@ -12,5 +12,12 @@ public interface AuthenticationHandler { void handleRegister(Request req, Response res); void handleChangePassword(Request req, Response res); + /** + * Other plugins may need to verify a user's identity using details from handleLogin() + * @param details The user's unique one-time token that needs to be verified + * @return If the verification was successful + */ + boolean verifyUser(String details); + LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData); } diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java index 67b3d4023..122e04ff6 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java @@ -28,6 +28,12 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler { res.send("Authentication is not available with the default authentication method"); } + @Override + public boolean verifyUser(String details) { + Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify")); + return false; + } + @Override public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) { LoginResultJson responseData = new LoginResultJson(); diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index c9c3c0c70..2b392b682 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -16,6 +16,9 @@ "no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.", "default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json." }, + "authentication": { + "default_unable_to_verify": "[Authentication] Something called the verifyUser method which is unavailable in the default authentication handler" + }, "no_commands_error": "Commands are not supported in dispatch only mode.", "unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s", "account": { From 39796099c8f630dfaf43e92fa27f2a8ae3baff9f Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Wed, 11 May 2022 18:25:01 +0800 Subject: [PATCH 278/434] Improve text & remove extra punctuation --- src/main/resources/languages/zh-CN.json | 100 ++++++++++++------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 2f9663f4f..01572a20b 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -55,7 +55,7 @@ "permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!", "console_execute_error": "此命令只能在服务器控制台执行呐~", "player_execute_error": "此命令只能在游戏内执行哦~", - "command_exist_error": "这条命令……好像找不到呢?。", + "command_exist_error": "这条命令...好像找不到呢?", "no_description_specified": "没有指定说明", "invalid": { "amount": "无效的数量。", @@ -97,19 +97,19 @@ "delete": "账号已删除。", "no_account": "账号不存在。", "command_usage": "用法:account <create|delete> <用户名> [uid]", - "description": "创建或删除账号。" + "description": "创建或删除账号" }, "broadcast": { "command_usage": "用法:broadcast <消息>", "message_sent": "公告已发送。", - "description": "向所有玩家发送公告。" + "description": "向所有玩家发送公告" }, "changescene": { "usage": "用法:changescene <场景ID>", "already_in_scene": "你已经在这个场景中了。", "success": "已切换至场景 %s。", "exists_error": "此场景不存在。", - "description": "切换指定场景。" + "description": "切换指定场景" }, "clear": { "command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", @@ -120,32 +120,32 @@ "displays": "已清空 %s 的屏幕。", "virtuals": "已清除 %s 的所有货币和经验值。", "everything": "已清除 %s 的所有物品。", - "description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品。" + "description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品" }, "coop": { "usage": "用法:coop <玩家ID> <目标玩家ID>", - "success": "已强制传送 %s 到 %s 的世界", - "description": "强制传送指定用户到他人的世界。" + "success": "已强制传送 %s 到 %s 的世界。", + "description": "强制传送指定用户到他人的世界" }, "enter_dungeon": { "usage": "用法:enterdungeon <秘境ID>", - "changed": "已进入秘境 %s", + "changed": "已进入秘境 %s。", "not_found_error": "此秘境不存在。", "in_dungeon_error": "你已经在秘境中了。", - "description": "进入指定秘境。" + "description": "进入指定秘境" }, "giveAll": { "usage": "用法:giveall [玩家] [数量]", "started": "正在给予全部物品...", "success": "已给予 %s 全部物品。", "invalid_amount_or_playerId": "无效的数量/玩家ID。", - "description": "给予所有物品。" + "description": "给予所有物品" }, "giveArtifact": { "usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]", "id_error": "无效的圣遗物ID。", "success": "已将 %s 给予 %s。", - "description": "给予指定圣遗物。" + "description": "给予指定圣遗物" }, "giveChar": { "usage": "用法:givechar <玩家> <角色ID|角色名> [数量]", @@ -153,50 +153,50 @@ "invalid_avatar_id": "无效的角色ID。", "invalid_avatar_level": "无效的角色等级。", "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。", - "description": "给予指定角色。" + "description": "给予指定角色" }, "give": { "usage": "用法:give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]", "refinement_only_applicable_weapons": "只有武器可以设置精炼等级。", "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", "given": "已将 %s 个 %s 给予 %s。", - "given_with_level_and_refinement": "已将 %s (等级 %s, 精炼 %s) %s 个给予 %s", - "given_level": "已将 %s (等级 %s) %s 个给予 %s", - "description": "给予指定物品。" + "given_with_level_and_refinement": "已将 %s (等级 %s, 精炼 %s) %s 个给予 %s。", + "given_level": "已将 %s (等级 %s) %s 个给予 %s。", + "description": "给予指定物品" }, "godmode": { - "success": "%s 的无敌模式已被设置为 %s。", - "description": "防止你受到伤害。" + "success": "%s 的上帝模式已被设置为 %s。", + "description": "防止你受到伤害" }, "heal": { "success": "已经治疗所有角色。", - "description": "治疗当前队伍的角色。" + "description": "治疗当前队伍的角色" }, "kick": { - "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", - "server_kick_player": "正在踢出玩家 [%s:%s]", - "description": "从服务器内踢出指定玩家。" + "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出。", + "server_kick_player": "正在踢出玩家 [%s:%s]...", + "description": "从服务器内踢出指定玩家" }, "kill": { "usage": "用法:killall [玩家UID] [场景ID]", - "scene_not_found_in_player_world": "未在玩家世界中找到此场景", + "scene_not_found_in_player_world": "未在玩家世界中找到此场景。", "kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。", - "description": "杀死所有怪物。" + "description": "杀死所有怪物" }, "killCharacter": { "usage": "用法:/killcharacter [玩家ID]", "success": "已杀死 %s 当前角色。", - "description": "杀死当前角色。" + "description": "杀死当前角色" }, "language": { "current_language": "当前语言是: %s", "language_changed": "语言切换至: %s", "language_not_found": "目前服务端没有这种语言: %s", - "description": "显示或切换当前语言。" + "description": "显示或切换当前语言" }, "list": { "success": "目前在线人数:%s", - "description": "查看所有玩家。" + "description": "查看所有玩家" }, "permission": { "usage": "用法:permission <add|remove> <用户名> <权限>", @@ -205,25 +205,25 @@ "remove": "权限已移除。", "not_have_error": "此玩家未拥有权限!", "account_error": "账号不存在。", - "description": "添加或移除指定玩家的权限。" + "description": "添加或移除指定玩家的权限" }, "position": { "success": "坐标:%s, %s, %s\n场景ID:%s", - "description": "获取所在位置。" + "description": "获取所在位置" }, "reload": { "reload_start": "正在重载配置文件和数据。", "reload_done": "重载完成。", - "description": "重载配置文件和数据。" + "description": "重载配置文件和数据" }, "resetConst": { "reset_all": "重置所有角色的命座。", "success": "已重置 %s 的命座,重新登录后生效。", - "description": "重置当前角色的命之座,执行命令后需重新登录以生效。" + "description": "重置当前角色的命之座,执行命令后需重新登录以生效" }, "resetShopLimit": { "usage": "用法:/resetshop <玩家ID>", - "description": "重置所选玩家的商店刷新时间。" + "description": "重置所选玩家的商店刷新时间" }, "sendMail": { "usage": "用法:give [玩家] <物品ID|物品名称> [数量]", @@ -246,19 +246,19 @@ "sender": "<发件人>", "arguments": "<物品ID|物品名称|finish> [数量] [等级]", "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。", - "description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化。" + "description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化" }, "sendMessage": { "usage": "用法:sendmessage <玩家> <消息>", "success": "消息已发送。", - "description": "向指定玩家发送消息。" + "description": "向指定玩家发送消息" }, "setFetterLevel": { "usage": "用法:setfetterlevel <好感度等级>", "range_error": "好感度等级必须在 0 到 10 之间。", - "success": "好感度已设置为 %s 级", + "success": "好感度已设置为 %s 级。", "level_error": "无效的好感度等级。", - "description": "设置当前角色的好感度等级。" + "description": "设置当前角色的好感度等级" }, "setStats": { "usage_console": "用法:setstats|stats @<UID> <属性> <数值>", @@ -270,23 +270,23 @@ "set_self": "%s 已设为 %s。", "set_for_uid": "将 %s (来自 %s) 设置为 %s。", "set_max_hp": "最大生命值已设为 %s。", - "description": "设置当前角色的属性。" + "description": "设置当前角色的属性" }, "setWorldLevel": { "usage": "用法:setworldlevel <等级>", "value_error": "世界等级必须设置在0-8之间。", "success": "已将世界等级设为 %s。", "invalid_world_level": "无效的世界等级。", - "description": "设置世界等级,执行命令后需重新登录以生效。" + "description": "设置世界等级,执行命令后需重新登录以生效" }, "spawn": { "usage": "用法:spawn <实体ID> [数量] [等级(仅怪物)]", "success": "已生成 %s 个 %s。", - "description": "在你附近生成一个生物。" + "description": "在你附近生成一个生物" }, "stop": { "success": "正在关闭服务器...", - "description": "停止服务器。" + "description": "停止服务器" }, "talent": { "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", @@ -303,20 +303,20 @@ "normal_attack_id": "普通攻击的 ID 为 %s。", "e_skill_id": "元素战技ID %s。", "q_skill_id": "元素爆发ID %s。", - "description": "设置当前角色的天赋等级。" + "description": "设置当前角色的天赋等级" }, "teleportAll": { - "success": "已将所有玩家传送到你的位置", + "success": "已将所有玩家传送到你的位置。", "error": "你只能在多人游戏状态下执行此命令。", - "description": "将你世界中的所有玩家传送到你所在的位置。" + "description": "将你世界中的所有玩家传送到你所在的位置" }, "teleport": { "usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]", "usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]", "specify_player_id": "你必须指定一个玩家ID。", "invalid_position": "无效的位置。", - "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s", - "description": "改变指定玩家的位置。" + "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s。", + "description": "改变指定玩家的位置" }, "tower": { "unlock_done": "深境回廊的所有层已全部解锁。" @@ -325,28 +325,28 @@ "usage": "用法:weather <天气ID> [气候ID]", "success": "已更改天气为 %s,气候为 %s。", "invalid_id": "无效的天气ID。", - "description": "更改天气。" + "description": "更改天气" }, "drop": { "command_usage": "用法:drop <物品ID|物品名称> [数量]", "success": "已丢下 %s 个 %s。", - "description": "在你附近丢下一个物品。" + "description": "在你附近丢下一个物品" }, "help": { "usage": "用法:", "aliases": "别名:", "available_commands": "可用命令:", - "description": "发送帮助信息或显示指定命令的信息。" + "description": "发送帮助信息或显示指定命令的信息" }, "restart": { - "description": "重新启动服务器。" + "description": "重新启动服务器" }, "unlocktower": { "success": "解锁完成。", - "description": "解锁深境螺旋的所有层。" + "description": "解锁深境螺旋的所有层" }, "resetshop": { - "description": "重置商店刷新时间。" + "description": "重置商店刷新时间" } } } From 07ad24262ed58a1efae3e5ddf4787cea3fed39b8 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Wed, 11 May 2022 19:37:52 +0800 Subject: [PATCH 279/434] Improve text --- src/main/resources/languages/zh-CN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 01572a20b..111e3c0c5 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -20,7 +20,7 @@ "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", "account": { "login_attempt": "[Dispatch] 客户端 %s 正在尝试登录", - "login_success": "[Dispatch] 客户端 %s 已登录,UID为 %s", + "login_success": "[Dispatch] 客户端 %s 已登录,UID 为 %s", "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录", "login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败", "login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录,UID 为 %s", @@ -96,7 +96,7 @@ "create": "已创建账号,UID 为 %s。", "delete": "账号已删除。", "no_account": "账号不存在。", - "command_usage": "用法:account <create|delete> <用户名> [uid]", + "command_usage": "用法:account <create|delete> <用户名> [UID]", "description": "创建或删除账号" }, "broadcast": { @@ -290,7 +290,7 @@ }, "talent": { "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", - "usage_2": "另一种设置天赋等级的方法:/talent <n (普攻) | e (元素战技) | q (元素爆发)> <数值>", + "usage_2": "另一种设置天赋等级的方法:/talent <n (普通攻击) | e (元素战技) | q (元素爆发)> <数值>", "usage_3": "获取天赋ID:/talent getid", "lower_16": "无效的天赋等级,天赋等级应小于等于15。", "set_id": "将天赋等级设为 %s。", From 9bdb1c762cfa7fb71bd2d4b25744aba80b84d537 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 18:34:30 -0700 Subject: [PATCH 280/434] Introduce `-version` argument to display version --- build.gradle | 16 ++++++++++++++++ src/main/java/emu/grasscutter/Grasscutter.java | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/build.gradle b/build.gradle index 4434ed28e..186a6d440 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,14 @@ targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' version = '1.1.1-dev' +def gitCommitHash = { + try { + return 'git rev-parse --verify --short HEAD'.execute().text.trim() + } catch (e) { + return "GIT_NOT_FOUND" + } +} + sourceCompatibility = 17 targetCompatibility = 17 @@ -97,6 +105,7 @@ application { mainClassName = 'emu.grasscutter.Grasscutter' } + jar { manifest { attributes 'Main-Class': 'emu.grasscutter.Grasscutter' @@ -113,6 +122,13 @@ jar { from('src/main/java') { include '*.xml' } + new File(projectDir, "src/generated/main/java/emu/grasscutter/BuildConfig.java").text = """ + package emu.grasscutter; + public class BuildConfig { + public static final String VERSION = \"${version}\"; + public static final String GIT_HASH = \"${gitCommitHash()}\"; + } + """ destinationDir = file(".") } diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 73e761e6e..a192815d1 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -29,6 +29,7 @@ import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; +import emu.grasscutter.BuildConfig; import javax.annotation.Nullable; @@ -82,6 +83,9 @@ public final class Grasscutter { case "-gachamap" -> { Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; } + case "-version" -> { + System.out.println("Grasscutter version: " + BuildConfig.VERSION + "\nGit Hash: " + BuildConfig.GIT_HASH); exitEarly = true; + } } } From c00a6dbadd3d61f2b30f9ad509f2190ddebe7656 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 21:11:19 -0700 Subject: [PATCH 281/434] Fix github action build issue * Move `BuildConfig.java` from `/src/generated`to `/src/main` to accomplish the building pipeline * Add BuildConfig.java to the .gitignore --- .gitignore | 1 + build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9fa6c9427..6fd78ed3b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ language/ languages/ gacha-mapping.js data/gacha_mappings.js +BuildConfig.java # macOS .DS_Store diff --git a/build.gradle b/build.gradle index 186a6d440..47f433b57 100644 --- a/build.gradle +++ b/build.gradle @@ -122,7 +122,7 @@ jar { from('src/main/java') { include '*.xml' } - new File(projectDir, "src/generated/main/java/emu/grasscutter/BuildConfig.java").text = """ + new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """ package emu.grasscutter; public class BuildConfig { public static final String VERSION = \"${version}\"; From edc75b2632318c99671cb089d6ed694750864c0c Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 21:36:30 -0700 Subject: [PATCH 282/434] Display version info at console starting --- src/main/java/emu/grasscutter/Grasscutter.java | 1 + src/main/resources/languages/en-US.json | 3 ++- src/main/resources/languages/pl-PL.json | 3 ++- src/main/resources/languages/zh-CN.json | 3 ++- src/main/resources/languages/zh-TW.json | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index a192815d1..b328a453b 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -190,6 +190,7 @@ public final class Grasscutter { } getLogger().info(translate("messages.status.done")); + getLogger().info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH)); String input = null; boolean isLastInterrupted = false; while (true) { diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 2b392b682..099e36eaa 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -48,7 +48,8 @@ "run_mode_error": "Invalid server run mode: %s.", "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", "create_resources": "Creating resources folder...", - "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder." + "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.", + "version": "Grasscutter version: %s, Git Hash: %s" } }, "commands": { diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json index 8f76d8951..0f5d88aa9 100644 --- a/src/main/resources/languages/pl-PL.json +++ b/src/main/resources/languages/pl-PL.json @@ -45,7 +45,8 @@ "run_mode_error": "Błędny tryb pracy serwera: %s.", "run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...", "create_resources": "Tworzenie folderu resources...", - "resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources." + "resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources.", + "version": "Grasscutter versión: %s, Git Hash: %s" } }, "commands": { diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 111e3c0c5..5f2b02736 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -45,7 +45,8 @@ "run_mode_error": "无效的服务器运行模式:%s。", "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "create_resources": "正在创建 resources 目录...", - "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。" + "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。", + "version": "Grasscutter版本: %s, Git Hash: %s" } }, "commands": { diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 9c3c99686..3cb850415 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -45,7 +45,8 @@ "run_mode_error": "無效的伺服器運行模式: %s。", "run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...", "create_resources": "正在建立 resources 資料夾...", - "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。" + "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。", + "version": "Grasscutter版本: %s, Git Hash: %s" } }, "commands": { From 6921ea77a9b78524ceab6f36e39aa97411d6af6b Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 23:23:58 -0700 Subject: [PATCH 283/434] Revise version format --- src/main/java/emu/grasscutter/Grasscutter.java | 2 +- src/main/resources/languages/en-US.json | 2 +- src/main/resources/languages/pl-PL.json | 2 +- src/main/resources/languages/zh-CN.json | 2 +- src/main/resources/languages/zh-TW.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index b328a453b..6ca78dfa9 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -84,7 +84,7 @@ public final class Grasscutter { Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; } case "-version" -> { - System.out.println("Grasscutter version: " + BuildConfig.VERSION + "\nGit Hash: " + BuildConfig.GIT_HASH); exitEarly = true; + System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH); exitEarly = true; } } } diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 099e36eaa..81e97eed7 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -49,7 +49,7 @@ "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", "create_resources": "Creating resources folder...", "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.", - "version": "Grasscutter version: %s, Git Hash: %s" + "version": "Grasscutter version: %s-%s" } }, "commands": { diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json index 0f5d88aa9..aa06723d8 100644 --- a/src/main/resources/languages/pl-PL.json +++ b/src/main/resources/languages/pl-PL.json @@ -46,7 +46,7 @@ "run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...", "create_resources": "Tworzenie folderu resources...", "resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources.", - "version": "Grasscutter versión: %s, Git Hash: %s" + "version": "Grasscutter versión: %s-%s" } }, "commands": { diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 5f2b02736..84d4a8c94 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -46,7 +46,7 @@ "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "create_resources": "正在创建 resources 目录...", "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。", - "version": "Grasscutter版本: %s, Git Hash: %s" + "version": "Grasscutter版本: %s-%s" } }, "commands": { diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 3cb850415..8e7a75949 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -46,7 +46,7 @@ "run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...", "create_resources": "正在建立 resources 資料夾...", "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。", - "version": "Grasscutter版本: %s, Git Hash: %s" + "version": "Grasscutter版本: %s-%s" } }, "commands": { From 48b5873feedd7a5648ba4c35e3dee2524d76a626 Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 23:34:53 -0700 Subject: [PATCH 284/434] Make `injectGitHash` as a task --- build.gradle | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 47f433b57..ffb6a498e 100644 --- a/build.gradle +++ b/build.gradle @@ -45,13 +45,6 @@ targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' version = '1.1.1-dev' -def gitCommitHash = { - try { - return 'git rev-parse --verify --short HEAD'.execute().text.trim() - } catch (e) { - return "GIT_NOT_FOUND" - } -} sourceCompatibility = 17 targetCompatibility = 17 @@ -122,13 +115,6 @@ jar { from('src/main/java') { include '*.xml' } - new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """ - package emu.grasscutter; - public class BuildConfig { - public static final String VERSION = \"${version}\"; - public static final String GIT_HASH = \"${gitCommitHash()}\"; - } - """ destinationDir = file(".") } @@ -242,6 +228,26 @@ javadoc { } } +task injectGitHash { + doLast { + def gitCommitHash = { + try { + return 'git rev-parse --verify --short HEAD'.execute().text.trim() + } catch (e) { + return "GIT_NOT_FOUND" + } + } + new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """ + package emu.grasscutter; + public class BuildConfig { + public static final String VERSION = \"${version}\"; + public static final String GIT_HASH = \"${gitCommitHash()}\"; + } + """ + } +} + processResources { dependsOn "generateProto" + dependsOn "injectGitHash" } From f16edfd58e9c9aeebc4592246c9f1da72fcb1ffe Mon Sep 17 00:00:00 2001 From: mingjun97 <my@lyric.today> Date: Tue, 10 May 2022 23:55:40 -0700 Subject: [PATCH 285/434] Fix building error --- build.gradle | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index ffb6a498e..3a8d2c34d 100644 --- a/build.gradle +++ b/build.gradle @@ -229,25 +229,22 @@ javadoc { } task injectGitHash { - doLast { - def gitCommitHash = { - try { - return 'git rev-parse --verify --short HEAD'.execute().text.trim() - } catch (e) { - return "GIT_NOT_FOUND" - } + def gitCommitHash = { + try { + return 'git rev-parse --verify --short HEAD'.execute().text.trim() + } catch (e) { + return "GIT_NOT_FOUND" } - new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """ - package emu.grasscutter; - public class BuildConfig { - public static final String VERSION = \"${version}\"; - public static final String GIT_HASH = \"${gitCommitHash()}\"; - } - """ } + new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """ + package emu.grasscutter; + public class BuildConfig { + public static final String VERSION = \"${version}\"; + public static final String GIT_HASH = \"${gitCommitHash()}\"; + } + """ } processResources { dependsOn "generateProto" - dependsOn "injectGitHash" } From 1d4e0e09d04f2e7427af35af355ac2dbe86a6fbe Mon Sep 17 00:00:00 2001 From: ImmuState <kyoko12@gmx.at> Date: Wed, 11 May 2022 11:19:25 -0700 Subject: [PATCH 286/434] Add gacha details page. --- data/gacha_details.html | 121 ++++++++++++++++++ .../grasscutter/game/gacha/GachaBanner.java | 9 +- .../grasscutter/game/gacha/GachaManager.java | 18 ++- .../server/dispatch/DispatchServer.java | 4 + .../dispatch/http/GachaDetailsHandler.java | 90 +++++++++++++ src/main/resources/languages/en-US.json | 9 ++ src/main/resources/languages/pl-PL.json | 9 ++ src/main/resources/languages/zh-CN.json | 9 ++ src/main/resources/languages/zh-TW.json | 9 ++ 9 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 data/gacha_details.html create mode 100644 src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java diff --git a/data/gacha_details.html b/data/gacha_details.html new file mode 100644 index 000000000..16cf7313a --- /dev/null +++ b/data/gacha_details.html @@ -0,0 +1,121 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css"> + <style> + body { + background-color: #f0f0f0; + } + p { + font-weight:300; + } + a,a:hover { + text-decoration:none !important; + color:#626976; + } + .content { + padding:3rem 0; + } + .container { + color:#626976; + position: relative; + } + + h2 { + font-size:20px; + } + h3 { + font-size:16px; + } + </style> + <title>Banner Details + + + +
+
+

{{TITLE}}

+ +

{{AVAILABLE_FIVE_STARS}}

+
+
    +
+ +

{{AVAILABLE_FOUR_STARS}}

+
+
    +
+ +

{{AVAILABLE_THREE_STARS}}

+
+
    +
+
+
+
+ +
+ + + + diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index dce433fcf..7a2646a4f 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -102,6 +102,11 @@ public class GachaBanner { + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; + String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" + + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + + "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType; + // Grasscutter.getLogger().info("record = " + record); GachaInfo.Builder info = GachaInfo.newBuilder() .setGachaType(this.getGachaType()) @@ -112,8 +117,8 @@ public class GachaBanner { .setCostItemNum(1) .setGachaPrefabPath(this.getPrefabPath()) .setGachaPreviewPrefabPath(this.getPreviewPrefabPath()) - .setGachaProbUrl(record) - .setGachaProbUrlOversea(record) + .setGachaProbUrl(details) + .setGachaProbUrlOversea(details) .setGachaRecordUrl(record) .setGachaRecordUrlOversea(record) .setTenCostItemId(this.getCostItem()) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 03edca09a..f0baf65a9 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -65,7 +65,23 @@ public class GachaManager { public Int2ObjectMap getGachaBanners() { return gachaBanners; } - + + public int[] getYellowAvatars() { + return this.yellowAvatars; + } + public int[] getYellowWeapons() { + return this.yellowWeapons; + } + public int[] getPurpleAvatars() { + return this.purpleAvatars; + } + public int[] getPurpleWeapons() { + return this.purpleWeapons; + } + public int[] getBlueWeapons() { + return this.blueWeapons; + } + public int randomRange(int min, int max) { return ThreadLocalRandom.current().nextInt(max - min + 1) + min; } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 4e09f8881..c78aff7c6 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -16,6 +16,7 @@ import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler; import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; +import emu.grasscutter.server.dispatch.http.GachaDetailsHandler; import emu.grasscutter.server.dispatch.http.GachaRecordHandler; import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; @@ -455,6 +456,9 @@ public final class DispatchServer { httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL); + // gacha details + httpServer.get("/gacha/details", new GachaDetailsHandler()); + // static file support for plugins httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java new file mode 100644 index 000000000..d46cead40 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java @@ -0,0 +1,90 @@ +package emu.grasscutter.server.dispatch.http; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.gacha.GachaBanner; +import emu.grasscutter.game.gacha.GachaManager; +import emu.grasscutter.game.gacha.GachaBanner.BannerType; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.http.HttpContextHandler; +import express.http.Request; +import express.http.Response; + +import static emu.grasscutter.utils.Language.translate; + +import static emu.grasscutter.Configuration.*; + +public final class GachaDetailsHandler implements HttpContextHandler { + private final String render_template; + + public GachaDetailsHandler() { + File template = new File(Utils.toFilePath(DATA("/gacha_details.html"))); + this.render_template = template.exists() ? new String(FileUtils.read(template)) : null; + } + + @Override + public void handle(Request req, Response res) throws IOException { + String response = this.render_template; + + // Get player info (for langauge). + String sessionKey = req.query("s"); + Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); + Player player = Grasscutter.getGameServer().getPlayerByUid(account.getPlayerUid()); + + // If the template was not loaded, return an error. + if (this.render_template == null) { + res.send(translate(player, "gacha.details.template_missing")); + return; + } + + // Add translated title etc. to the page. + response = response.replace("{{TITLE}}", translate(player, "gacha.details.title")); + response = response.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars")); + response = response.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars")); + response = response.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars")); + + // Get the banner info for the banner we want. + int gachaType = Integer.parseInt(req.query("gachaType")); + GachaManager manager = Grasscutter.getGameServer().getGachaManager(); + GachaBanner banner = manager.getGachaBanners().get(gachaType); + + // Add 5-star items. + Set fiveStarItems = new LinkedHashSet<>(); + + Arrays.stream(banner.getRateUpItems1()).forEach(i -> fiveStarItems.add(Integer.toString(i))); + if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.EVENT) { + Arrays.stream(manager.getYellowAvatars()).forEach(i -> fiveStarItems.add(Integer.toString(i))); + } + if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.WEAPON) { + Arrays.stream(manager.getYellowWeapons()).forEach(i -> fiveStarItems.add(Integer.toString(i))); + } + + response = response.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]"); + + // Add 4-star items. + Set fourStarItems = new LinkedHashSet<>(); + + Arrays.stream(banner.getRateUpItems2()).forEach(i -> fourStarItems.add(Integer.toString(i))); + Arrays.stream(manager.getPurpleAvatars()).forEach(i -> fourStarItems.add(Integer.toString(i))); + Arrays.stream(manager.getPurpleWeapons()).forEach(i -> fourStarItems.add(Integer.toString(i))); + + response = response.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]"); + + // Add 3-star items. + Set threeStarItems = new LinkedHashSet<>(); + Arrays.stream(manager.getBlueWeapons()).forEach(i -> threeStarItems.add(Integer.toString(i))); + response = response.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]"); + + // Done. + res.send(response); + } +} diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 81e97eed7..42b52d7f5 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -352,5 +352,14 @@ "resetshop": { "description": "reset shop" } + }, + "gacha": { + "details": { + "title": "Banner Details", + "available_five_stars": "Available 5-star Items", + "available_four_stars": "Available 4-star Items", + "available_three_stars": "Available 3-star Items", + "template_missing": "data/gacha_details.html is missing." + } } } diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json index aa06723d8..e5eff2d84 100644 --- a/src/main/resources/languages/pl-PL.json +++ b/src/main/resources/languages/pl-PL.json @@ -302,5 +302,14 @@ "resetshop": { "description": "zresetuj sklep" } + }, + "gacha": { + "details": { + "title": "Banner Details", + "available_five_stars": "Available 5-star Items", + "available_four_stars": "Available 4-star Items", + "available_three_stars": "Available 3-star Items", + "template_missing": "data/gacha_details.html is missing." + } } } \ No newline at end of file diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 84d4a8c94..ac46370d5 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -349,5 +349,14 @@ "resetshop": { "description": "重置商店刷新时间" } + }, + "gacha": { + "details": { + "title": "Banner Details", + "available_five_stars": "Available 5-star Items", + "available_four_stars": "Available 4-star Items", + "available_three_stars": "Available 3-star Items", + "template_missing": "data/gacha_details.html is missing." + } } } diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 8e7a75949..f6ae52c9d 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -302,5 +302,14 @@ "resetshop": { "description": "重置商店時間" } + }, + "gacha": { + "details": { + "title": "Banner Details", + "available_five_stars": "Available 5-star Items", + "available_four_stars": "Available 4-star Items", + "available_three_stars": "Available 3-star Items", + "template_missing": "data/gacha_details.html is missing." + } } } From 5ff8a4514ed797544b46e39adae87bd2541f1cff Mon Sep 17 00:00:00 2001 From: ImmuState Date: Wed, 11 May 2022 11:36:14 -0700 Subject: [PATCH 287/434] Insert language setting based on the player's account. --- data/gacha_details.html | 10 +++++----- .../server/dispatch/http/GachaDetailsHandler.java | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/data/gacha_details.html b/data/gacha_details.html index 16cf7313a..ccd775ef6 100644 --- a/data/gacha_details.html +++ b/data/gacha_details.html @@ -38,7 +38,7 @@

{{TITLE}}

- +

{{AVAILABLE_FIVE_STARS}}


    @@ -81,11 +81,11 @@ @@ -161,32 +128,12 @@ } return "" + itemID + ""; } - function dateFormatter(timeStamp) { - var date = new Date(timeStamp); - if (lang == "en-us" || lang == null) { // MM/DD/YYYY hh:mm:ss.SSS - return String(date.getMonth()+1).padStart(2, "0") + - "/"+String(date.getDate()).padStart(2, "0")+ - "/"+date.getFullYear()+ - " "+String(date.getHours()).padStart(2, "0")+ - ":"+String(date.getMinutes()).padStart(2, "0")+ - ":"+String(date.getSeconds()).padStart(2, "0")+ - "."+String(date.getMilliseconds()).padStart(3, "0"); - } else if (lang == "zh-cn") { // YYYY/MM/DD hh:mm:ss.SSS - return date.getFullYear()+ - "/" + String(date.getMonth()+1).padStart(2, "0") + - "/"+String(date.getDate()).padStart(2, "0")+ - " "+String(date.getHours()).padStart(2, "0")+ - ":"+String(date.getMinutes()).padStart(2, "0")+ - ":"+String(date.getSeconds()).padStart(2, "0")+ - "."+String(date.getMilliseconds()).padStart(3, "0"); - } - } (function (){ var container = document.getElementById("container"); record.forEach(element => { var e = document.createElement("tr"); - e.innerHTML= "" + dateFormatter(element.time) + "" + itemMapper(element.item) + ""; + e.innerHTML= "" + (new Date(element.time).toLocaleString(lang)) + "" + itemMapper(element.item) + ""; container.appendChild(e); }); // setup pagenation buttons From 6c6e206a880f6e8cda7f19a51d106de8d9289099 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Fri, 13 May 2022 14:59:05 +0800 Subject: [PATCH 308/434] fix: LEAK: ByteBuf.release() was not called --- src/main/java/emu/grasscutter/server/game/GameSession.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index 7cc9a799f..cf6386770 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -252,6 +252,7 @@ public class GameSession extends KcpChannel { } catch (Exception e) { e.printStackTrace(); } finally { + data.release(); packet.release(); } } From 608c379afec188c4f16ba75ea911f3723b67fcdd Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 13 May 2022 03:12:25 -0700 Subject: [PATCH 309/434] Switch to using quest excels --- .../java/emu/grasscutter/data/GameData.java | 13 +- .../emu/grasscutter/data/ResourceLoader.java | 17 +-- .../data/custom/MainQuestData.java | 53 ++++++++ .../grasscutter/data/custom/QuestConfig.java | 25 ---- .../data/custom/QuestConfigData.java | 104 ---------------- .../emu/grasscutter/data/def/QuestData.java | 115 ++++++++++++++++++ .../grasscutter/game/quest/GameMainQuest.java | 1 - .../emu/grasscutter/game/quest/GameQuest.java | 49 +++++--- .../grasscutter/game/quest/QuestManager.java | 27 ++-- .../grasscutter/game/quest/QuestValue.java | 4 +- .../game/quest/ServerQuestHandler.java | 12 +- .../game/quest/conditions/BaseCondition.java | 6 +- .../ConditionPlayerLevelEqualGreater.java | 8 +- .../quest/conditions/ConditionStateEqual.java | 10 +- .../game/quest/content/BaseContent.java | 6 +- .../quest/content/ContentCompleteTalk.java | 6 +- ...uestTriggerType.java => QuestTrigger.java} | 4 +- .../game/quest/handlers/QuestBaseHandler.java | 2 +- .../server/packet/recv/HandlerNpcTalkReq.java | 4 +- ...etServerCondMeetQuestListUpdateNotify.java | 3 - .../java/emu/grasscutter/tools/Tools.java | 12 +- 21 files changed, 262 insertions(+), 219 deletions(-) create mode 100644 src/main/java/emu/grasscutter/data/custom/MainQuestData.java delete mode 100644 src/main/java/emu/grasscutter/data/custom/QuestConfig.java delete mode 100644 src/main/java/emu/grasscutter/data/custom/QuestConfigData.java create mode 100644 src/main/java/emu/grasscutter/data/def/QuestData.java rename src/main/java/emu/grasscutter/game/quest/enums/{QuestTriggerType.java => QuestTrigger.java} (99%) diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 2b40818e1..75b840202 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -12,7 +12,7 @@ import emu.grasscutter.data.custom.AbilityEmbryoEntry; import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; -import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.data.custom.MainQuestData; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.*; import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; @@ -28,7 +28,7 @@ public class GameData { private static final Map abilityModifiers = new HashMap<>(); private static final Map openConfigEntries = new HashMap<>(); private static final Map scenePointEntries = new HashMap<>(); - private static final Int2ObjectMap questConfigs = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap mainQuestData = new Int2ObjectOpenHashMap<>(); // ExcelConfigs private static final Int2ObjectMap playerLevelDataMap = new Int2ObjectOpenHashMap<>(); @@ -70,6 +70,7 @@ public class GameData { private static final Int2ObjectMap worldLevelDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap dailyDungeonDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap dungeonDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap questDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap shopGoodsDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap combineDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap rewardPreviewDataMap = new Int2ObjectOpenHashMap<>(); @@ -124,8 +125,8 @@ public class GameData { return getScenePointEntries().get(sceneId + "_" + pointId); } - public static Int2ObjectMap getQuestConfigs() { - return questConfigs; + public static Int2ObjectMap getMainQuestDataMap() { + return mainQuestData; } public static Int2ObjectMap getAvatarDataMap() { @@ -337,4 +338,8 @@ public class GameData { public static Int2ObjectMap getTowerScheduleDataMap(){ return towerScheduleDataMap; } + + public static Int2ObjectMap getQuestDataMap() { + return questDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 5c2ac1ee6..4b940c44d 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -24,9 +24,7 @@ import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; -import emu.grasscutter.data.custom.QuestConfig; -import emu.grasscutter.data.custom.QuestConfigData; -import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; +import emu.grasscutter.data.custom.MainQuestData; import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.game.world.SpawnDataEntry.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -407,24 +405,19 @@ public class ResourceLoader { } for (File file : folder.listFiles()) { - QuestConfigData mainQuest = null; + MainQuestData mainQuest = null; try (FileReader fileReader = new FileReader(file)) { - mainQuest = Grasscutter.getGsonFactory().fromJson(fileReader, QuestConfigData.class); + mainQuest = Grasscutter.getGsonFactory().fromJson(fileReader, MainQuestData.class); } catch (Exception e) { e.printStackTrace(); continue; } - if (mainQuest.getSubQuests() != null) { - for (SubQuestConfigData subQuest : mainQuest.getSubQuests()) { - QuestConfig quest = new QuestConfig(mainQuest, subQuest); - GameData.getQuestConfigs().put(quest.getId(), quest); - } - } + GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest); } - Grasscutter.getLogger().info("Loaded " + GameData.getQuestConfigs().size() + " Quest Configs"); + Grasscutter.getLogger().info("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas."); } // BinOutput configs diff --git a/src/main/java/emu/grasscutter/data/custom/MainQuestData.java b/src/main/java/emu/grasscutter/data/custom/MainQuestData.java new file mode 100644 index 000000000..e405e3598 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/MainQuestData.java @@ -0,0 +1,53 @@ +package emu.grasscutter.data.custom; + +import emu.grasscutter.game.quest.enums.LogicType; +import emu.grasscutter.game.quest.enums.QuestTrigger; +import emu.grasscutter.game.quest.enums.QuestType; + +public class MainQuestData { + private int id; + private int series; + private QuestType type; + + private long titleTextMapHash; + private int[] suggestTrackMainQuestList; + private int[] rewardIdList; + + private SubQuestData[] subQuests; + + public int getId() { + return id; + } + + public int getSeries() { + return series; + } + + public QuestType getType() { + return type; + } + + public long getTitleTextMapHash() { + return titleTextMapHash; + } + + public int[] getSuggestTrackMainQuestList() { + return suggestTrackMainQuestList; + } + + public int[] getRewardIdList() { + return rewardIdList; + } + + public SubQuestData[] getSubQuests() { + return subQuests; + } + + public static class SubQuestData { + private int subId; + + public int getSubId() { + return subId; + } + } +} diff --git a/src/main/java/emu/grasscutter/data/custom/QuestConfig.java b/src/main/java/emu/grasscutter/data/custom/QuestConfig.java deleted file mode 100644 index 8674ff7ab..000000000 --- a/src/main/java/emu/grasscutter/data/custom/QuestConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package emu.grasscutter.data.custom; - -import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; - -public class QuestConfig { - private final QuestConfigData mainQuest; - private final SubQuestConfigData subQuest; - - public QuestConfig(QuestConfigData mainQuest, SubQuestConfigData subQuest) { - this.mainQuest = mainQuest; - this.subQuest = subQuest; - } - - public int getId() { - return subQuest.getSubId(); - } - - public QuestConfigData getMainQuest() { - return mainQuest; - } - - public SubQuestConfigData getSubQuest() { - return subQuest; - } -} diff --git a/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java b/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java deleted file mode 100644 index 3ede024f2..000000000 --- a/src/main/java/emu/grasscutter/data/custom/QuestConfigData.java +++ /dev/null @@ -1,104 +0,0 @@ -package emu.grasscutter.data.custom; - -import emu.grasscutter.game.quest.enums.LogicType; -import emu.grasscutter.game.quest.enums.QuestTriggerType; -import emu.grasscutter.game.quest.enums.QuestType; - -public class QuestConfigData { - private int id; - private int series; - private QuestType type; - - private long titleTextMapHash; - private int[] suggestTrackMainQuestList; - private int[] rewardIdList; - - private SubQuestConfigData[] subQuests; - - public int getId() { - return id; - } - - public int getSeries() { - return series; - } - - public QuestType getType() { - return type; - } - - public long getTitleTextMapHash() { - return titleTextMapHash; - } - - public int[] getSuggestTrackMainQuestList() { - return suggestTrackMainQuestList; - } - - public int[] getRewardIdList() { - return rewardIdList; - } - - public SubQuestConfigData[] getSubQuests() { - return subQuests; - } - - public class SubQuestConfigData { - private int subId; - private int mainId; - - private LogicType acceptCondComb; - private QuestCondition[] acceptCond; - - private LogicType finishCondComb; - private QuestCondition[] finishCond; - - private LogicType failCondComb; - private QuestCondition[] failCond; - - public int getSubId() { - return subId; - } - - public int getMainId() { - return mainId; - } - - public LogicType getAcceptCondComb() { - return acceptCondComb; - } - - public QuestCondition[] getAcceptCond() { - return acceptCond; - } - - public LogicType getFinishCondComb() { - return finishCondComb; - } - - public QuestCondition[] getFinishCond() { - return finishCond; - } - - public LogicType getFailCondComb() { - return failCondComb; - } - - public QuestCondition[] getFailCond() { - return failCond; - } - } - - public class QuestCondition { - private QuestTriggerType type; - private int[] param; - - public QuestTriggerType getType() { - return type; - } - - public int[] getParam() { - return param; - } - } -} diff --git a/src/main/java/emu/grasscutter/data/def/QuestData.java b/src/main/java/emu/grasscutter/data/def/QuestData.java new file mode 100644 index 000000000..31ac2ce7e --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/QuestData.java @@ -0,0 +1,115 @@ +package emu.grasscutter.data.def; + +import java.util.Arrays; +import java.util.List; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.game.quest.enums.LogicType; +import emu.grasscutter.game.quest.enums.QuestTrigger; + +@ResourceType(name = "QuestExcelConfigData.json") +public class QuestData extends GameResource { + private int SubId; + private int MainId; + private int Order; + private long DescTextMapHash; + + private LogicType AcceptCondComb; + private QuestCondition[] acceptConditons; + private LogicType FinishCondComb; + private QuestCondition[] finishConditons; + private LogicType FailCondComb; + private QuestCondition[] failConditons; + + private List AcceptCond; + private List FinishCond; + private List FailCond; + private List BeginExec; + private List FinishExec; + private List FailExec; + + public int getId() { + return SubId; + } + + public int getMainId() { + return MainId; + } + + public int getOrder() { + return Order; + } + + public long getDescTextMapHash() { + return DescTextMapHash; + } + + public LogicType getAcceptCondComb() { + return AcceptCondComb; + } + + public QuestCondition[] getAcceptCond() { + return acceptConditons; + } + + public LogicType getFinishCondComb() { + return FinishCondComb; + } + + public QuestCondition[] getFinishCond() { + return finishConditons; + } + + public LogicType getFailCondComb() { + return FailCondComb; + } + + public QuestCondition[] getFailCond() { + return failConditons; + } + + public void onLoad() { + this.acceptConditons = AcceptCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new); + AcceptCond = null; + this.finishConditons = FinishCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new); + FinishCond = null; + this.failConditons = FailCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new); + FailCond = null; + } + + public class QuestParam { + QuestTrigger Type; + int[] Param; + String count; + } + + public class QuestExecParam { + QuestTrigger Type; + String[] Param; + String count; + } + + public static class QuestCondition { + private QuestTrigger type; + private int[] param; + private String count; + + public QuestCondition(QuestParam param) { + this.type = param.Type; + this.param = param.Param; + } + + public QuestTrigger getType() { + return type; + } + + public int[] getParam() { + return param; + } + + public String getCount() { + return count; + } + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java index 1ceda3356..bf88b8efe 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java @@ -10,7 +10,6 @@ import dev.morphia.annotations.Id; import dev.morphia.annotations.Indexed; import dev.morphia.annotations.Transient; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.custom.QuestConfig; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.quest.enums.ParentQuestState; diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java index d3a240a07..b242166eb 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -2,9 +2,11 @@ package emu.grasscutter.game.quest; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Transient; -import emu.grasscutter.data.custom.QuestConfig; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; -import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.custom.MainQuestData; +import emu.grasscutter.data.custom.MainQuestData.SubQuestData; +import emu.grasscutter.data.def.QuestData; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.quest.enums.LogicType; import emu.grasscutter.game.quest.enums.QuestState; @@ -16,7 +18,7 @@ import emu.grasscutter.utils.Utils; @Entity public class GameQuest { @Transient private GameMainQuest mainQuest; - @Transient private QuestConfig config; + @Transient private QuestData questData; private int questId; private int mainQuestId; @@ -32,21 +34,21 @@ public class GameQuest { @Deprecated // Morphia only. Do not use. public GameQuest() {} - public GameQuest(GameMainQuest mainQuest, QuestConfig config) { + public GameQuest(GameMainQuest mainQuest, QuestData questData) { this.mainQuest = mainQuest; - this.questId = config.getId(); - this.mainQuestId = config.getMainQuest().getId(); - this.config = config; + this.questId = questData.getId(); + this.mainQuestId = questData.getMainId(); + this.questData = questData; this.acceptTime = Utils.getCurrentSeconds(); this.startTime = this.acceptTime; this.state = QuestState.QUEST_STATE_UNFINISHED; - if (config.getSubQuest().getFinishCond() != null) { - this.finishProgressList = new int[config.getSubQuest().getFinishCond().length]; + if (questData.getFinishCond()!= null) { + this.finishProgressList = new int[questData.getFinishCond().length]; } - if (config.getSubQuest().getFailCond() != null) { - this.failProgressList = new int[config.getSubQuest().getFailCond().length]; + if (questData.getFailCond() != null) { + this.failProgressList = new int[questData.getFailCond().length]; } this.mainQuest.getChildQuests().put(this.questId, this); @@ -72,13 +74,13 @@ public class GameQuest { return mainQuestId; } - public QuestConfig getConfig() { - return config; + public QuestData getData() { + return questData; } - public void setConfig(QuestConfig config) { + public void setConfig(QuestData config) { if (this.getQuestId() != config.getId()) return; - this.config = config; + this.questData = config; } public QuestState getState() { @@ -148,16 +150,23 @@ public class GameQuest { public boolean tryAcceptQuestLine() { try { - for (SubQuestConfigData questData : getConfig().getMainQuest().getSubQuests()) { - GameQuest quest = getMainQuest().getChildQuestById(questData.getSubId()); + MainQuestData questConfig = GameData.getMainQuestDataMap().get(this.getMainQuestId()); + for (SubQuestData subQuest : questConfig.getSubQuests()) { + GameQuest quest = getMainQuest().getChildQuestById(subQuest.getSubId()); if (quest == null) { + QuestData questData = GameData.getQuestDataMap().get(subQuest.getSubId()); + + if (questData == null) { + continue; + } + int[] accept = new int[questData.getAcceptCond().length]; // TODO for (int i = 0; i < questData.getAcceptCond().length; i++) { QuestCondition condition = questData.getAcceptCond()[i]; - boolean result = getOwner().getServer().getQuestHandler().triggerCondition(this, condition); + boolean result = getOwner().getServer().getQuestHandler().triggerCondition(this, condition, condition.getParam()); accept[i] = result ? 1 : 0; } @@ -165,7 +174,7 @@ public class GameQuest { boolean shouldAccept = LogicType.calculate(questData.getAcceptCondComb(), accept); if (shouldAccept) { - this.getOwner().getQuestManager().addQuest(questData.getSubId()); + this.getOwner().getQuestManager().addQuest(questData.getId()); } } } diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index 76c098b07..548e8241a 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -9,12 +9,11 @@ import java.util.function.Consumer; import java.util.function.Function; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.custom.QuestConfig; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; -import emu.grasscutter.data.custom.QuestConfigData.SubQuestConfigData; +import emu.grasscutter.data.def.QuestData; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.enums.LogicType; import emu.grasscutter.game.quest.enums.QuestState; import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; @@ -46,12 +45,12 @@ public class QuestManager { } public GameQuest getQuestById(int questId) { - QuestConfig questConfig = GameData.getQuestConfigs().get(questId); + QuestData questConfig = GameData.getQuestDataMap().get(questId); if (questConfig == null) { return null; } - GameMainQuest mainQuest = getQuests().get(questConfig.getMainQuest().getId()); + GameMainQuest mainQuest = getQuests().get(questConfig.getMainId()); if (mainQuest == null) { return null; @@ -79,8 +78,8 @@ public class QuestManager { } } - public GameMainQuest addMainQuest(QuestConfig questConfig) { - GameMainQuest mainQuest = new GameMainQuest(getPlayer(), questConfig.getMainQuest().getId()); + public GameMainQuest addMainQuest(QuestData questConfig) { + GameMainQuest mainQuest = new GameMainQuest(getPlayer(), questConfig.getMainId()); getQuests().put(mainQuest.getParentQuestId(), mainQuest); getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(mainQuest)); @@ -89,13 +88,13 @@ public class QuestManager { } public GameQuest addQuest(int questId) { - QuestConfig questConfig = GameData.getQuestConfigs().get(questId); + QuestData questConfig = GameData.getQuestDataMap().get(questId); if (questConfig == null) { return null; } // Main quest - GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainQuest().getId()); + GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId()); // Create main quest if it doesnt exist if (mainQuest == null) { @@ -122,11 +121,11 @@ public class QuestManager { return quest; } - public void triggerEvent(QuestTriggerType condType, int... params) { + public void triggerEvent(QuestTrigger condType, int... params) { Set changedQuests = new HashSet<>(); this.forEachActiveQuest(quest -> { - SubQuestConfigData data = quest.getConfig().getSubQuest(); + QuestData data = quest.getData(); for (int i = 0; i < data.getFinishCond().length; i++) { if (quest.getFinishProgressList()[i] == 1) { @@ -150,7 +149,7 @@ public class QuestManager { }); for (GameQuest quest : changedQuests) { - LogicType logicType = quest.getConfig().getSubQuest().getFailCondComb(); + LogicType logicType = quest.getData().getFailCondComb(); int[] progress = quest.getFinishProgressList(); // Handle logical comb @@ -174,7 +173,7 @@ public class QuestManager { for (GameQuest quest : mainQuest.getChildQuests().values()) { quest.setMainQuest(mainQuest); - quest.setConfig(GameData.getQuestConfigs().get(quest.getQuestId())); + quest.setConfig(GameData.getQuestDataMap().get(quest.getQuestId())); } this.getQuests().put(mainQuest.getParentQuestId(), mainQuest); diff --git a/src/main/java/emu/grasscutter/game/quest/QuestValue.java b/src/main/java/emu/grasscutter/game/quest/QuestValue.java index 3042ad5de..42b868fc8 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestValue.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestValue.java @@ -3,9 +3,9 @@ package emu.grasscutter.game.quest; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; @Retention(RetentionPolicy.RUNTIME) public @interface QuestValue { - QuestTriggerType value(); + QuestTrigger value(); } diff --git a/src/main/java/emu/grasscutter/game/quest/ServerQuestHandler.java b/src/main/java/emu/grasscutter/game/quest/ServerQuestHandler.java index 1c269de90..36c929ab3 100644 --- a/src/main/java/emu/grasscutter/game/quest/ServerQuestHandler.java +++ b/src/main/java/emu/grasscutter/game/quest/ServerQuestHandler.java @@ -4,11 +4,9 @@ import java.util.Set; import org.reflections.Reflections; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.server.game.GameServer; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -62,7 +60,7 @@ public class ServerQuestHandler { public boolean triggerCondition(GameQuest quest, QuestCondition condition, int... params) { QuestBaseHandler handler = condHandlers.get(condition.getType().getValue()); - if (handler == null || quest.getConfig() == null) { + if (handler == null || quest.getData() == null) { return false; } @@ -72,7 +70,7 @@ public class ServerQuestHandler { public boolean triggerContent(GameQuest quest, QuestCondition condition, int... params) { QuestBaseHandler handler = contHandlers.get(condition.getType().getValue()); - if (handler == null || quest.getConfig() == null) { + if (handler == null || quest.getData() == null) { return false; } @@ -82,7 +80,7 @@ public class ServerQuestHandler { public boolean triggerExec(GameQuest quest, QuestCondition condition, int... params) { QuestBaseHandler handler = execHandlers.get(condition.getType().getValue()); - if (handler == null || quest.getConfig() == null) { + if (handler == null || quest.getData() == null) { return false; } diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/BaseCondition.java b/src/main/java/emu/grasscutter/game/quest/conditions/BaseCondition.java index 903773f0e..d94e60c22 100644 --- a/src/main/java/emu/grasscutter/game/quest/conditions/BaseCondition.java +++ b/src/main/java/emu/grasscutter/game/quest/conditions/BaseCondition.java @@ -1,12 +1,12 @@ package emu.grasscutter.game.quest.conditions; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.QuestValue; import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -@QuestValue(QuestTriggerType.QUEST_CONTENT_NONE) +@QuestValue(QuestTrigger.QUEST_CONTENT_NONE) public class BaseCondition extends QuestBaseHandler { @Override diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPlayerLevelEqualGreater.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPlayerLevelEqualGreater.java index f5df2b13c..3e3db87fb 100644 --- a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPlayerLevelEqualGreater.java +++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPlayerLevelEqualGreater.java @@ -1,17 +1,17 @@ package emu.grasscutter.game.quest.conditions; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.QuestValue; import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -@QuestValue(QuestTriggerType.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER) +@QuestValue(QuestTrigger.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER) public class ConditionPlayerLevelEqualGreater extends QuestBaseHandler { @Override public boolean execute(GameQuest quest, QuestCondition condition, int... params) { - return quest.getOwner().getLevel() >= condition.getParam()[0]; + return quest.getOwner().getLevel() >= params[0]; } } diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionStateEqual.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionStateEqual.java index 71b44c967..37ecc6d30 100644 --- a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionStateEqual.java +++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionStateEqual.java @@ -1,20 +1,20 @@ package emu.grasscutter.game.quest.conditions; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.QuestValue; import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -@QuestValue(QuestTriggerType.QUEST_COND_STATE_EQUAL) +@QuestValue(QuestTrigger.QUEST_COND_STATE_EQUAL) public class ConditionStateEqual extends QuestBaseHandler { @Override public boolean execute(GameQuest quest, QuestCondition condition, int... params) { - GameQuest checkQuest = quest.getOwner().getQuestManager().getQuestById(condition.getParam()[0]); + GameQuest checkQuest = quest.getOwner().getQuestManager().getQuestById(params[0]); if (checkQuest != null) { - return checkQuest.getState().getValue() == condition.getParam()[1]; + return checkQuest.getState().getValue() == params[1]; } return false; diff --git a/src/main/java/emu/grasscutter/game/quest/content/BaseContent.java b/src/main/java/emu/grasscutter/game/quest/content/BaseContent.java index 820d6f133..ce700896d 100644 --- a/src/main/java/emu/grasscutter/game/quest/content/BaseContent.java +++ b/src/main/java/emu/grasscutter/game/quest/content/BaseContent.java @@ -1,12 +1,12 @@ package emu.grasscutter.game.quest.content; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.QuestValue; import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -@QuestValue(QuestTriggerType.QUEST_CONTENT_NONE) +@QuestValue(QuestTrigger.QUEST_CONTENT_NONE) public class BaseContent extends QuestBaseHandler { @Override diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentCompleteTalk.java b/src/main/java/emu/grasscutter/game/quest/content/ContentCompleteTalk.java index aad196306..3423519ec 100644 --- a/src/main/java/emu/grasscutter/game/quest/content/ContentCompleteTalk.java +++ b/src/main/java/emu/grasscutter/game/quest/content/ContentCompleteTalk.java @@ -1,12 +1,12 @@ package emu.grasscutter.game.quest.content; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.QuestValue; import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.quest.handlers.QuestBaseHandler; -@QuestValue(QuestTriggerType.QUEST_CONTENT_COMPLETE_TALK) +@QuestValue(QuestTrigger.QUEST_CONTENT_COMPLETE_TALK) public class ContentCompleteTalk extends QuestBaseHandler { @Override diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestTriggerType.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestTrigger.java similarity index 99% rename from src/main/java/emu/grasscutter/game/quest/enums/QuestTriggerType.java rename to src/main/java/emu/grasscutter/game/quest/enums/QuestTrigger.java index cf8dabdba..def3a399d 100644 --- a/src/main/java/emu/grasscutter/game/quest/enums/QuestTriggerType.java +++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestTrigger.java @@ -1,6 +1,6 @@ package emu.grasscutter.game.quest.enums; -public enum QuestTriggerType { +public enum QuestTrigger { QUEST_COND_NONE (0), QUEST_COND_STATE_EQUAL (1), QUEST_COND_STATE_NOT_EQUAL (2), @@ -225,7 +225,7 @@ public enum QuestTriggerType { private final int value; - QuestTriggerType(int id) { + QuestTrigger(int id) { this.value = id; } diff --git a/src/main/java/emu/grasscutter/game/quest/handlers/QuestBaseHandler.java b/src/main/java/emu/grasscutter/game/quest/handlers/QuestBaseHandler.java index 68bf88361..5a3514200 100644 --- a/src/main/java/emu/grasscutter/game/quest/handlers/QuestBaseHandler.java +++ b/src/main/java/emu/grasscutter/game/quest/handlers/QuestBaseHandler.java @@ -1,6 +1,6 @@ package emu.grasscutter.game.quest.handlers; -import emu.grasscutter.data.custom.QuestConfigData.QuestCondition; +import emu.grasscutter.data.def.QuestData.QuestCondition; import emu.grasscutter.game.quest.GameQuest; public abstract class QuestBaseHandler { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java index 515552289..82248c98c 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java @@ -1,7 +1,7 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.quest.enums.QuestTriggerType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.NpcTalkReqOuterClass.NpcTalkReq; @@ -16,7 +16,7 @@ public class HandlerNpcTalkReq extends PacketHandler { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { NpcTalkReq req = NpcTalkReq.parseFrom(payload); - session.getPlayer().getQuestManager().triggerEvent(QuestTriggerType.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId()); + session.getPlayer().getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId()); session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId())); } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java index b2ea3d577..fa2e8ab81 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketServerCondMeetQuestListUpdateNotify.java @@ -1,9 +1,6 @@ package emu.grasscutter.server.packet.send; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.custom.QuestConfig; import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.GameMainQuest; import emu.grasscutter.game.quest.GameQuest; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 8b28c027e..5b0f563ee 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -19,10 +19,11 @@ import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; -import emu.grasscutter.data.custom.QuestConfig; +import emu.grasscutter.data.custom.MainQuestData; import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.MonsterData; +import emu.grasscutter.data.def.QuestData; import emu.grasscutter.data.def.SceneData; import emu.grasscutter.utils.Utils; @@ -149,13 +150,16 @@ final class ToolsWithLanguageOption { writer.println(data.getId() + " : " + data.getScriptData()); } + writer.println(); + writer.println("// Quests"); - list = new ArrayList<>(GameData.getQuestConfigs().keySet()); + list = new ArrayList<>(GameData.getQuestDataMap().keySet()); Collections.sort(list); for (Integer id : list) { - QuestConfig data = GameData.getQuestConfigs().get(id); - writer.println(data.getId() + " : " + map.get(data.getMainQuest().getTitleTextMapHash())); + QuestData data = GameData.getQuestDataMap().get(id); + MainQuestData mainQuest = GameData.getMainQuestDataMap().get(data.getMainId()); + writer.println(data.getId() + " : " + map.get(mainQuest.getTitleTextMapHash()) + " - " + map.get(data.getDescTextMapHash())); } writer.println(); From f53b533dfb01fca058cb42fe13098b68e4c52577 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 13 May 2022 05:33:43 -0700 Subject: [PATCH 310/434] Add one more quest trigger --- .../game/dungeons/DungeonManager.java | 4 +++- .../game/quest/content/ContentEnterDungeon.java | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/main/java/emu/grasscutter/game/quest/content/ContentEnterDungeon.java diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index 0a68e6ab0..5c0d1fd27 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -7,6 +7,7 @@ import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.DungeonData; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.SceneType; +import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.server.game.GameServer; @@ -51,8 +52,9 @@ public class DungeonManager { int sceneId = data.getSceneId(); player.getScene().setPrevScene(sceneId); - if(player.getWorld().transferPlayerToScene(player, sceneId, data)){ + if (player.getWorld().transferPlayerToScene(player, sceneId, data)) { player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver); + player.getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_ENTER_DUNGEON, data.getId()); } player.getScene().setPrevScenePoint(pointId); diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentEnterDungeon.java b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterDungeon.java new file mode 100644 index 000000000..e00e59f9a --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterDungeon.java @@ -0,0 +1,17 @@ +package emu.grasscutter.game.quest.content; + +import emu.grasscutter.data.def.QuestData.QuestCondition; +import emu.grasscutter.game.quest.QuestValue; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.enums.QuestTrigger; +import emu.grasscutter.game.quest.handlers.QuestBaseHandler; + +@QuestValue(QuestTrigger.QUEST_CONTENT_ENTER_DUNGEON) +public class ContentEnterDungeon extends QuestBaseHandler { + + @Override + public boolean execute(GameQuest quest, QuestCondition condition, int... params) { + return condition.getParam()[0] == params[0]; + } + +} From 3f3ab11ef60ba22a9b811dfe7232736a98563fea Mon Sep 17 00:00:00 2001 From: AnimeGitB Date: Fri, 6 May 2022 23:39:45 +0930 Subject: [PATCH 311/434] Gacha rework Add fallback stripping and C6 stripping Converting banner definitions from pity vars to lerp arrays Properly implement rates and pool smoothing Also move reusable functions to Utils --- data/Banners.json | 19 +- .../grasscutter/game/gacha/GachaBanner.java | 82 +++-- .../grasscutter/game/gacha/GachaManager.java | 314 ++++++++++-------- .../game/gacha/PlayerGachaBannerInfo.java | 84 ++++- .../java/emu/grasscutter/utils/Utils.java | 66 ++++ 5 files changed, 397 insertions(+), 168 deletions(-) diff --git a/data/Banners.json b/data/Banners.json index a4f724ac9..1aaf39cb7 100644 --- a/data/Banners.json +++ b/data/Banners.json @@ -10,8 +10,9 @@ "beginTime": 0, "endTime": 1924992000, "sortId": 1000, - "rateUpItems1": [], - "rateUpItems2": [] + "fallbackItems4Pool1": [1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064], + "weights4": [[1,510], [8,510], [10,10000]], + "weights5": [[1,75], [73,150], [90,10000]] }, { "gachaType": 301, @@ -24,9 +25,10 @@ "beginTime": 0, "endTime": 1924992000, "sortId": 9998, - "maxItemType": 1, - "rateUpItems1": [1002], - "rateUpItems2": [1053, 1020, 1045] + "rateUpItems4": [1053, 1020, 1045], + "rateUpItems5": [1002], + "fallbackItems5Pool2": [], + "weights5": [[1,80], [73,80], [90,10000]] }, { "gachaType": 302, @@ -39,11 +41,12 @@ "beginTime": 0, "endTime": 1924992000, "sortId": 9997, - "minItemType": 2, "eventChance": 75, "softPity": 80, "hardPity": 80, - "rateUpItems1": [11509, 12504], - "rateUpItems2": [11401, 12402, 13407, 14401, 15401] + "rateUpItems4": [11401, 12402, 13407, 14401, 15401], + "rateUpItems5": [11509, 12504], + "fallbackItems5Pool1": [], + "weights5": [[1,100], [62,100], [73, 7800], [80,10000]] } ] diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 7a2646a4f..52630768c 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -2,6 +2,7 @@ package emu.grasscutter.game.gacha; import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo; import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo; +import emu.grasscutter.utils.Utils; import static emu.grasscutter.Configuration.*; @@ -15,14 +16,31 @@ public class GachaBanner { private int beginTime; private int endTime; private int sortId; - private int[] rateUpItems1; - private int[] rateUpItems2; - private int baseYellowWeight = 60; // Max 10000 - private int basePurpleWeight = 510; // Max 10000 - private int eventChance = 50; // Chance to win a featured event item - private int softPity = 75; - private int hardPity = 90; + private int[] rateUpItems4 = {}; + private int[] rateUpItems5 = {}; + private int[] fallbackItems3 = {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304}; + private int[] fallbackItems4Pool1 = {1014, 1020, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064}; + private int[] fallbackItems4Pool2 = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405}; + private int[] fallbackItems5Pool1 = {1003, 1016, 1042, 1035, 1041}; + private int[] fallbackItems5Pool2 = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}; + private boolean removeC6FromPool = false; + private boolean autoStripRateUpFromFallback = true; + private int[][] weights4 = {{1,510}, {8,510}, {10,10000}}; + private int[][] weights5 = {{1,75}, {73,150}, {90,10000}}; + private int[][] poolBalanceWeights4 = {{1,255}, {17,255}, {21,10455}}; + private int[][] poolBalanceWeights5 = {{1,30}, {147,150}, {181,10230}}; + private int eventChance4 = 50; // Chance to win a featured event item + private int eventChance5 = 50; // Chance to win a featured event item private BannerType bannerType = BannerType.STANDARD; + + // Kinda wanna deprecate these but they're in people's configs + private int[] rateUpItems1 = {}; + private int[] rateUpItems2 = {}; + private int softPity = -1; + private int hardPity = -1; + private int eventChance = -1; + private int baseYellowWeight = -1; + private int basePurpleWeight = -1; public int getGachaType() { return gachaType; @@ -72,24 +90,42 @@ public class GachaBanner { return basePurpleWeight; } - public int[] getRateUpItems1() { - return rateUpItems1; + public int[] getRateUpItems4() { + return (rateUpItems2.length > 0) ? rateUpItems2 : rateUpItems4; + } + public int[] getRateUpItems5() { + return (rateUpItems1.length > 0) ? rateUpItems1 : rateUpItems5; } - public int[] getRateUpItems2() { - return rateUpItems2; - } - - public int getSoftPity() { - return softPity - 1; + public int[] getFallbackItems3() {return fallbackItems3;} + public int[] getFallbackItems4Pool1() {return fallbackItems4Pool1;} + public int[] getFallbackItems4Pool2() {return fallbackItems4Pool2;} + public int[] getFallbackItems5Pool1() {return fallbackItems5Pool1;} + public int[] getFallbackItems5Pool2() {return fallbackItems5Pool2;} + + public boolean getRemoveC6FromPool() {return removeC6FromPool;} + public boolean getAutoStripRateUpFromFallback() {return autoStripRateUpFromFallback;} + + + public int getWeight(int rarity, int pity) { + return switch(rarity) { + case 4 -> Utils.lerp(pity, weights4); + default -> Utils.lerp(pity, weights5); + }; } - public int getHardPity() { - return hardPity - 1; + public int getPoolBalanceWeight(int rarity, int pity) { + return switch(rarity) { + case 4 -> Utils.lerp(pity, poolBalanceWeights4); + default -> Utils.lerp(pity, poolBalanceWeights5); + }; } - public int getEventChance() { - return eventChance; + public int getEventChance(int rarity) { + return switch(rarity) { + case 4 -> eventChance4; + default -> (eventChance > -1) ? eventChance : eventChance5; + }; } @Deprecated @@ -131,10 +167,10 @@ public class GachaBanner { info.setGachaTitlePath(this.getTitlePath()); } - if (this.getRateUpItems1().length > 0) { + if (this.getRateUpItems5().length > 0) { GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1); - for (int id : getRateUpItems1()) { + for (int id : getRateUpItems5()) { upInfo.addItemIdList(id); info.addMainNameId(id); } @@ -142,10 +178,10 @@ public class GachaBanner { info.addGachaUpInfoList(upInfo); } - if (this.getRateUpItems2().length > 0) { + if (this.getRateUpItems4().length > 0) { GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2); - for (int id : getRateUpItems2()) { + for (int id : getRateUpItems4()) { upInfo.addItemIdList(id); if (info.getSubNameIdCount() == 0) { info.addSubNameId(id); diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 160377913..e3c0153e0 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -29,6 +29,7 @@ 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 emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; @@ -42,12 +43,6 @@ public class GachaManager { private final Int2ObjectMap gachaBanners; private GetGachaInfoRsp cachedProto; WatchService watchService; - - private final int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041}; - private final int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}; - private final 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}; - private final int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405}; - private final int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304}; private static final int starglitterId = 221; private static final int stardustId = 222; @@ -66,24 +61,8 @@ public class GachaManager { public Int2ObjectMap getGachaBanners() { return gachaBanners; } - - public int[] getYellowAvatars() { - return this.yellowAvatars; - } - public int[] getYellowWeapons() { - return this.yellowWeapons; - } - public int[] getPurpleAvatars() { - return this.purpleAvatars; - } - public int[] getPurpleWeapons() { - return this.purpleWeapons; - } - public int[] getBlueWeapons() { - return this.blueWeapons; - } - - public int randomRange(int min, int max) { + + public int randomRange(int min, int max) { // Both are inclusive return ThreadLocalRandom.current().nextInt(max - min + 1) + min; } @@ -98,8 +77,14 @@ public class GachaManager { if(banners.size() > 0) { for (GachaBanner banner : banners) { getGachaBanners().put(banner.getGachaType(), banner); + Grasscutter.getLogger().info(String.format("Testing lerp code for banner gachaType %d :", banner.getGachaType())); // TODO: remove this before merging! + for (int i=1; i<91; i++) { + Grasscutter.getLogger().info(String.format("Pity %d : Weight %d", i, banner.getWeight(5, i))); + } } Grasscutter.getLogger().info("Banners successfully loaded."); + + this.cachedProto = createProto(); } else { Grasscutter.getLogger().error("Unable to load banners. Banners size is 0."); @@ -109,6 +94,139 @@ public class GachaManager { e.printStackTrace(); } } + + private class BannerPools { + public int[] rateUpItems4; + public int[] rateUpItems5; + public int[] fallbackItems4Pool1; + public int[] fallbackItems4Pool2; + public int[] fallbackItems5Pool1; + public int[] fallbackItems5Pool2; + + public BannerPools(GachaBanner banner) { + rateUpItems4 = banner.getRateUpItems4(); + rateUpItems5 = banner.getRateUpItems5(); + fallbackItems4Pool1 = banner.getFallbackItems4Pool1(); + fallbackItems4Pool2 = banner.getFallbackItems4Pool2(); + fallbackItems5Pool1 = banner.getFallbackItems5Pool1(); + fallbackItems5Pool2 = banner.getFallbackItems5Pool2(); + + if (banner.getAutoStripRateUpFromFallback()) { + fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, rateUpItems4); + fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, rateUpItems4); + fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, rateUpItems5); + fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, rateUpItems5); + } + } + + public void removeFromAllPools(int[] itemIds) { + rateUpItems4 = Utils.setSubtract(rateUpItems4, itemIds); + rateUpItems5 = Utils.setSubtract(rateUpItems5, itemIds); + fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, itemIds); + fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, itemIds); + fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, itemIds); + fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, itemIds); + } + } + + private synchronized int checkPlayerAvatarConstellationLevel(Player player, int itemId) { // Maybe this would be useful in the Player class? + ItemData itemData = GameData.getItemDataMap().get(itemId); + if ((itemData == null) || (itemData.getMaterialType() != MaterialType.MATERIAL_AVATAR)){ + return -2; // Not an Avatar + } + Avatar avatar = player.getAvatars().getAvatarById((itemId % 1000) + 10000000); + if (avatar == null) { + return -1; // Doesn't have + } + // Constellation + int constLevel = avatar.getCoreProudSkillLevel(); + GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId + 100); + constLevel += (constItem == null)? 0 : constItem.getCount(); + return constLevel; + } + + private synchronized int[] removeC6FromPool(int[] itemPool, Player player) { + IntList temp = new IntArrayList(); + for (int itemId : itemPool) { + if (checkPlayerAvatarConstellationLevel(player, itemId) < 6) { + temp.add(itemId); + } + } + return temp.toIntArray(); + } + + private synchronized int drawRoulette(int[] weights, int cutoff) { + // This follows the logic laid out in issue #183 + // Simple weighted selection with an upper bound for the roll that cuts off trailing entries + // All weights must be >= 0 + int total = 0; + for (int i : weights) { + if (i < 0) { + throw new IllegalArgumentException("Weights must be non-negative!"); + } + total += i; + } + int roll = ThreadLocalRandom.current().nextInt((total < cutoff)? total : cutoff); + int subTotal = 0; + for (int i : weights) { + subTotal += i; + if (roll < subTotal) { + return i; + } + } + // throw new IllegalStateException(); + return 0; // This should only be reachable if total==0 + } + + private synchronized int doRarePull(int[] featured, int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) { + int itemId = 0; + if ( (featured.length > 0) + && (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) + || (this.randomRange(1, 100) <= banner.getEventChance(rarity))) { + itemId = getRandom(featured); + gachaInfo.setFailedFeaturedItemPulls(rarity, 0); + } else { + gachaInfo.addFailedFeaturedItemPulls(rarity, 1); + if (fallback1.length < 1) { + itemId = getRandom(fallback2); // Don't ever run an empty fallback2 btw + } else { + int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1)); + int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2)); + int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly + case 1 -> 1 + drawRoulette(new int[] {pityPool1, pityPool2}, 10000); + default -> 2 - drawRoulette(new int[] {pityPool2, pityPool1}, 10000); + }; + itemId = switch (chosenPool) { + case 1: + gachaInfo.setPityPool(rarity, 1, 0); + yield getRandom(fallback1); + default: + gachaInfo.setPityPool(rarity, 2, 0); + yield getRandom(fallback2); + }; + } + } + return itemId; + } + + private synchronized int doPull(GachaBanner banner, PlayerGachaBannerInfo gachaInfo, BannerPools pools) { + // Pre-increment all pity pools (yes this makes all calculations assume 1-indexed pity) + gachaInfo.incPityAll(); + + int[] weights = {banner.getWeight(5, gachaInfo.getPity5()), banner.getWeight(4, gachaInfo.getPity4()), 10000}; + int levelWon = 5 - drawRoulette(weights, 10000); + + return switch (levelWon) { + case 5: + gachaInfo.setPity5(0); + yield doRarePull(pools.rateUpItems5, pools.fallbackItems5Pool1, pools.fallbackItems5Pool2, 5, banner, gachaInfo); + case 4: + gachaInfo.setPity4(0); + yield doRarePull(pools.rateUpItems4, pools.fallbackItems4Pool1, pools.fallbackItems4Pool2, 4, banner, gachaInfo); + default: + yield getRandom(banner.getFallbackItems3()); + }; + } public synchronized void doPulls(Player player, int gachaType, int times) { // Sanity check @@ -132,84 +250,27 @@ public class GachaManager { return; } - // Roll - PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner); - IntList wonItems = new IntArrayList(times); - - for (int i = 0; i < times; i++) { - int random = this.randomRange(1, 10000); - int itemId = 0; - - int bonusYellowChance = gachaInfo.getPity5() >= banner.getSoftPity() ? 100 * (gachaInfo.getPity5() - banner.getSoftPity() - 1): 0; - int yellowChance = banner.getBaseYellowWeight() + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance; - int purpleChance = 10000 - (banner.getBasePurpleWeight() + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f))); - - if (random <= yellowChance || gachaInfo.getPity5() >= banner.getHardPity()) { - if (banner.getRateUpItems1().length > 0) { - int eventChance = this.randomRange(1, 100); - - if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) { - itemId = getRandom(banner.getRateUpItems1()); - gachaInfo.setFailedFeaturedItemPulls(0); - } else { - // Lost the 50/50... rip - gachaInfo.addFailedFeaturedItemPulls(1); - } - } - - if (itemId == 0) { - int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2); - if (typeChance == 1) { - itemId = getRandom(this.yellowAvatars); - } else { - itemId = getRandom(this.yellowWeapons); - } - } - - // Pity - gachaInfo.addPity4(1); - gachaInfo.setPity5(0); - } else if (random >= purpleChance || gachaInfo.getPity4() >= 9) { - if (banner.getRateUpItems2().length > 0) { - int eventChance = this.randomRange(1, 100); - - if (eventChance >= 50) { - itemId = getRandom(banner.getRateUpItems2()); - } - } - - if (itemId == 0) { - int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2); - if (typeChance == 1) { - itemId = getRandom(this.purpleAvatars); - } else { - itemId = getRandom(this.purpleWeapons); - } - } - - // Pity - gachaInfo.addPity5(1); - gachaInfo.setPity4(0); - } else { - itemId = getRandom(this.blueWeapons); - - // Pity - gachaInfo.addPity4(1); - gachaInfo.addPity5(1); - } - - // Add winning item - wonItems.add(itemId); - } - // Add to character + PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner); + BannerPools pools = new BannerPools(banner); List list = new ArrayList<>(); int stardust = 0, starglitter = 0; + + if (banner.getRemoveC6FromPool()) { // The ultimate form of pity (non-vanilla) + pools.rateUpItems4 = removeC6FromPool(pools.rateUpItems4, player); + pools.rateUpItems5 = removeC6FromPool(pools.rateUpItems5, player); + pools.fallbackItems4Pool1 = removeC6FromPool(pools.fallbackItems4Pool1, player); + pools.fallbackItems4Pool2 = removeC6FromPool(pools.fallbackItems4Pool2, player); + pools.fallbackItems5Pool1 = removeC6FromPool(pools.fallbackItems5Pool1, player); + pools.fallbackItems5Pool2 = removeC6FromPool(pools.fallbackItems5Pool2, player); + } - for (int itemId : wonItems) { + for (int i = 0; i < times; i++) { + // Roll + int itemId = doPull(banner, gachaInfo, pools); ItemData itemData = GameData.getItemDataMap().get(itemId); if (itemData == null) { - continue; + continue; // Maybe we should bail out if an item fails instead of rolling the rest? } // Write gacha record @@ -222,44 +283,33 @@ public class GachaManager { boolean isTransferItem = false; // Const check - if (itemData.getMaterialType() == MaterialType.MATERIAL_AVATAR) { - int avatarId = (itemData.getId() % 1000) + 10000000; - Avatar avatar = player.getAvatars().getAvatarById(avatarId); - if (avatar != null) { - int constLevel = avatar.getCoreProudSkillLevel(); - int constItemId = itemData.getId() + 100; - GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); - if (constItem != null) { - constLevel += constItem.getCount(); + int constellation = checkPlayerAvatarConstellationLevel(player, itemId); + switch (constellation) { + case -2: // Is weapon + switch (itemData.getRankLevel()) { + case 5 -> addStarglitter = 10; + case 4 -> addStarglitter = 2; + default -> addStardust = 15; } - - if (constLevel < 6) { - // Not max const - addStarglitter = 2; - // Add 1 const + break; + case -1: // New character + gachaItem.setIsGachaItemNew(true); + break; + default: + if (constellation >= 6) { // C6, give consolation starglitter + addStarglitter = (itemData.getRankLevel()==5)? 25 : 5; + } else { // C0-C5, give constellation item + if (banner.getRemoveC6FromPool() && constellation == 5) { // New C6, remove it from the pools so we don't get C7 in a 10pull + pools.removeFromAllPools(new int[] {itemId}); + } + addStarglitter = (itemData.getRankLevel()==5)? 10 : 2; + int constItemId = itemId + 100; + GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null)); player.getInventory().addItem(constItemId, 1); - } else { - // Is max const - addStarglitter = 5; } - - if (itemData.getRankLevel() == 5) { - addStarglitter *= 5; - } - isTransferItem = true; - } else { - // New - gachaItem.setIsGachaItemNew(true); - } - } else { - // Is weapon - switch (itemData.getRankLevel()) { - case 5 -> addStarglitter = 10; - case 4 -> addStarglitter = 2; - case 3 -> addStardust = 15; - } + break; } // Create item @@ -272,7 +322,8 @@ public class GachaManager { if (addStardust > 0) { gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust)); - } if (addStarglitter > 0) { + } + if (addStarglitter > 0) { ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build(); if (isTransferItem) { gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam)); @@ -286,7 +337,8 @@ public class GachaManager { // Add stardust/starglitter if (stardust > 0) { player.getInventory().addItem(stardustId, stardust); - } if (starglitter > 0) { + } + if (starglitter > 0) { player.getInventory().addItem(starglitterId, starglitter); } diff --git a/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java b/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java index b0c85d355..f07d2eff0 100644 --- a/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java +++ b/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java @@ -7,6 +7,11 @@ public class PlayerGachaBannerInfo { private int pity5 = 0; private int pity4 = 0; private int failedFeaturedItemPulls = 0; + private int failedFeatured4ItemPulls = 0; + private int pity5Pool1 = 0; + private int pity5Pool2 = 0; + private int pity4Pool1 = 0; + private int pity4Pool2 = 0; public int getPity5() { return pity5; @@ -32,15 +37,82 @@ public class PlayerGachaBannerInfo { this.pity4 += amount; } - public int getFailedFeaturedItemPulls() { - return failedFeaturedItemPulls; + public int getFailedFeaturedItemPulls(int rarity) { + return switch (rarity) { + case 4 -> failedFeatured4ItemPulls; + default -> failedFeaturedItemPulls; // 5 + }; } - public void setFailedFeaturedItemPulls(int failedEventCharacterPulls) { - this.failedFeaturedItemPulls = failedEventCharacterPulls; + public void setFailedFeaturedItemPulls(int rarity, int amount) { + switch (rarity) { + case 4 -> failedFeatured4ItemPulls = amount; + default -> failedFeaturedItemPulls = amount; // 5 + }; } - public void addFailedFeaturedItemPulls(int amount) { - failedFeaturedItemPulls += amount; + public void addFailedFeaturedItemPulls(int rarity, int amount) { + switch (rarity) { + case 4 -> failedFeatured4ItemPulls += amount; + default -> failedFeaturedItemPulls += amount; // 5 + }; + } + + public int getPityPool(int rarity, int pool) { + return switch (rarity) { + case 4 -> switch (pool) { + case 1 -> pity4Pool1; + default -> pity4Pool2; + }; + default -> switch (pool) { + case 1 -> pity5Pool1; + default -> pity5Pool2; + }; + }; + } + + public void setPityPool(int rarity, int pool, int amount) { + switch (rarity) { + case 4: + switch (pool) { + case 1 -> pity4Pool1 = amount; + default -> pity4Pool2 = amount; + }; + break; + case 5: + default: + switch (pool) { + case 1 -> pity5Pool1 = amount; + default -> pity5Pool2 = amount; + }; + break; + }; + } + + public void addPityPool(int rarity, int pool, int amount) { + switch (rarity) { + case 4: + switch (pool) { + case 1 -> pity4Pool1 += amount; + default -> pity4Pool2 += amount; + }; + break; + case 5: + default: + switch (pool) { + case 1 -> pity5Pool1 += amount; + default -> pity5Pool2 += amount; + }; + break; + }; + } + + public void incPityAll() { + pity4++; + pity5++; + pity4Pool1++; + pity4Pool2++; + pity5Pool1++; + pity5Pool2++; } } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 4af62bfb4..58fc83cd0 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -15,6 +15,8 @@ import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; import org.slf4j.Logger; @@ -314,4 +316,68 @@ public final class Utils { return String.format("%s-%s", locale.getLanguage(), locale.getCountry()); } + /** + * Performs a linear interpolation using a table of fixed points to create an effective piecewise f(x) = y function. + * @param x + * @param xyArray Array of points in [[x0,y0], ... [xN, yN]] format + * @return f(x) = y + */ + public static int lerp(int x, int[][] xyArray) { + try { + if (x <= xyArray[0][0]){ // Clamp to first point + return xyArray[0][1]; + } else if (x >= xyArray[xyArray.length-1][0]) { // Clamp to last point + return xyArray[xyArray.length-1][1]; + } + // At this point we're guaranteed to have two lerp points, and pity be somewhere between them. + for (int i=0; i < xyArray.length-1; i++) { + if (x == xyArray[i+1][0]) { + return xyArray[i+1][1]; + } + if (x < xyArray[i+1][0]) { + // We are between [i] and [i+1], interpolation time! + // Using floats would be slightly cleaner but we can just as easily use ints if we're careful with order of operations. + int position = x - xyArray[i][0]; + int fullDist = xyArray[i+1][0] - xyArray[i][0]; + int prevValue = xyArray[i][1]; + int fullDelta = xyArray[i+1][1] - prevValue; + return prevValue + ( (position * fullDelta) / fullDist ); + } + } + } catch (IndexOutOfBoundsException e) { + Grasscutter.getLogger().error("Malformed lerp point array. Must be of form [[x0, y0], ..., [xN, yN]]."); + } + return 0; + } + + /** + * Checks if an int is in an int[] + * @param key int to look for + * @param array int[] to look in + * @return key in array + */ + public static boolean intInArray(int key, int[] array) { + for (int i : array) { + if (i == key) { + return true; + } + } + return false; + } + + /** + * Return a copy of minuend without any elements found in subtrahend. + * @param minuend The array we want elements from + * @param subtrahend The array whose elements we don't want + * @return The array with only the elements we want, in the order that minuend had them + */ + public static int[] setSubtract(int[] minuend, int[] subtrahend) { + IntList temp = new IntArrayList(); + for (int i : minuend) { + if (!intInArray(i, subtrahend)) { + temp.add(i); + } + } + return temp.toIntArray(); + } } From cda841dd62c310e80b36eaf8a991a5ad6c324426 Mon Sep 17 00:00:00 2001 From: AnimeGitB Date: Wed, 11 May 2022 19:24:59 +0930 Subject: [PATCH 312/434] Custom costs for different gacha pulls --- .../grasscutter/game/gacha/GachaBanner.java | 29 ++++++++++--------- .../grasscutter/game/gacha/GachaManager.java | 17 ++++++----- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 52630768c..3c97e671c 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -6,13 +6,18 @@ import emu.grasscutter.utils.Utils; import static emu.grasscutter.Configuration.*; +import emu.grasscutter.data.common.ItemParamData; + public class GachaBanner { private int gachaType; private int scheduleId; private String prefabPath; private String previewPrefabPath; private String titlePath; - private int costItem; + private int costItemId = 0; + private int costItemAmount = 1; + private int costItemId10 = 0; + private int costItemAmount10 = 10; private int beginTime; private int endTime; private int sortId; @@ -36,11 +41,8 @@ public class GachaBanner { // Kinda wanna deprecate these but they're in people's configs private int[] rateUpItems1 = {}; private int[] rateUpItems2 = {}; - private int softPity = -1; - private int hardPity = -1; private int eventChance = -1; - private int baseYellowWeight = -1; - private int basePurpleWeight = -1; + private int costItem = 0; public int getGachaType() { return gachaType; @@ -66,8 +68,15 @@ public class GachaBanner { return titlePath; } + public ItemParamData getCost(int numRolls) { + return switch (numRolls) { + case 10 -> new ItemParamData((costItemId10 > 0) ? costItemId10 : getCostItem(), costItemAmount10); + default -> new ItemParamData(getCostItem(), costItemAmount * numRolls); + }; + } + public int getCostItem() { - return costItem; + return (costItem > 0) ? costItem : costItemId; } public int getBeginTime() { @@ -82,14 +91,6 @@ public class GachaBanner { return sortId; } - public int getBaseYellowWeight() { - return baseYellowWeight; - } - - public int getBasePurpleWeight() { - return basePurpleWeight; - } - public int[] getRateUpItems4() { return (rateUpItems2.length > 0) ? rateUpItems2 : rateUpItems4; } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index e3c0153e0..11945c3d3 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -19,6 +19,7 @@ import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.gacha.GachaBanner.BannerType; import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.inventory.MaterialType; import emu.grasscutter.game.player.Player; @@ -233,7 +234,8 @@ public class GachaManager { if (times != 10 && times != 1) { return; } - if (player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) { + Inventory inventory = player.getInventory(); + if (inventory.getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > inventory.getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) { player.sendPacket(new PacketDoGachaRsp()); return; } @@ -246,7 +248,8 @@ public class GachaManager { } // Spend currency - if (banner.getCostItem() > 0 && !player.getInventory().payItem(banner.getCostItem(), times)) { + ItemParamData cost = banner.getCost(times); + if (cost.getCount() > 0 && !inventory.payItem(cost)) { return; } @@ -304,9 +307,9 @@ public class GachaManager { } addStarglitter = (itemData.getRankLevel()==5)? 10 : 2; int constItemId = itemId + 100; - GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); + GameItem constItem = inventory.getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null)); - player.getInventory().addItem(constItemId, 1); + inventory.addItem(constItemId, 1); } isTransferItem = true; break; @@ -315,7 +318,7 @@ public class GachaManager { // Create item GameItem item = new GameItem(itemData); gachaItem.setGachaItem(item.toItemParam()); - player.getInventory().addItem(item); + inventory.addItem(item); stardust += addStardust; starglitter += addStarglitter; @@ -336,10 +339,10 @@ public class GachaManager { // Add stardust/starglitter if (stardust > 0) { - player.getInventory().addItem(stardustId, stardust); + inventory.addItem(stardustId, stardust); } if (starglitter > 0) { - player.getInventory().addItem(starglitterId, starglitter); + inventory.addItem(starglitterId, starglitter); } // Packets From 5f9ac000173b5a3d742996f64ebd8336b89eaa87 Mon Sep 17 00:00:00 2001 From: AnimeGitB Date: Thu, 12 May 2022 00:14:26 +0930 Subject: [PATCH 313/434] Remove debug log from gacha --- src/main/java/emu/grasscutter/game/gacha/GachaManager.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 11945c3d3..a0337be7f 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -17,7 +17,6 @@ import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.data.def.ItemData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.gacha.GachaBanner.BannerType; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.inventory.ItemType; @@ -78,10 +77,6 @@ public class GachaManager { if(banners.size() > 0) { for (GachaBanner banner : banners) { getGachaBanners().put(banner.getGachaType(), banner); - Grasscutter.getLogger().info(String.format("Testing lerp code for banner gachaType %d :", banner.getGachaType())); // TODO: remove this before merging! - for (int i=1; i<91; i++) { - Grasscutter.getLogger().info(String.format("Pity %d : Weight %d", i, banner.getWeight(5, i))); - } } Grasscutter.getLogger().info("Banners successfully loaded."); From a89377d55855514a8f05db73f4eaa592497bd853 Mon Sep 17 00:00:00 2001 From: AnimeGitB Date: Thu, 12 May 2022 02:23:51 +0930 Subject: [PATCH 314/434] Updated cost logic and default weights --- data/Banners.json | 9 ++++-- .../grasscutter/game/gacha/GachaBanner.java | 12 ++++---- .../grasscutter/game/gacha/GachaManager.java | 30 ++++++++++++------- .../server/packet/send/PacketDoGachaRsp.java | 11 ++++--- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/data/Banners.json b/data/Banners.json index 1aaf39cb7..17e720e65 100644 --- a/data/Banners.json +++ b/data/Banners.json @@ -6,7 +6,9 @@ "prefabPath": "GachaShowPanel_A022", "previewPrefabPath": "UI_Tab_GachaShowPanel_A022", "titlePath": "UI_GACHA_SHOW_PANEL_A022_TITLE", - "costItem": 224, + "costItemId": 224, + "costItemAmount": 1, + "costItemAmount10": 10, "beginTime": 0, "endTime": 1924992000, "sortId": 1000, @@ -21,7 +23,7 @@ "prefabPath": "GachaShowPanel_A079", "previewPrefabPath": "UI_Tab_GachaShowPanel_A079", "titlePath": "UI_GACHA_SHOW_PANEL_A048_TITLE", - "costItem": 223, + "costItemId": 223, "beginTime": 0, "endTime": 1924992000, "sortId": 9998, @@ -37,7 +39,7 @@ "prefabPath": "GachaShowPanel_A080", "previewPrefabPath": "UI_Tab_GachaShowPanel_A080", "titlePath": "UI_GACHA_SHOW_PANEL_A021_TITLE", - "costItem": 223, + "costItemId": 223, "beginTime": 0, "endTime": 1924992000, "sortId": 9997, @@ -47,6 +49,7 @@ "rateUpItems4": [11401, 12402, 13407, 14401, 15401], "rateUpItems5": [11509, 12504], "fallbackItems5Pool1": [], + "weights4": [[1,600], [7,600], [8, 6600], [10,12600]], "weights5": [[1,100], [62,100], [73, 7800], [80,10000]] } ] diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 3c97e671c..1586198c2 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -6,6 +6,7 @@ import emu.grasscutter.utils.Utils; import static emu.grasscutter.Configuration.*; +import emu.grasscutter.Grasscutter; import emu.grasscutter.data.common.ItemParamData; public class GachaBanner { @@ -145,25 +146,26 @@ public class GachaBanner { + "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); + ItemParamData costItem1 = this.getCost(1); + ItemParamData costItem10 = this.getCost(10); GachaInfo.Builder info = GachaInfo.newBuilder() .setGachaType(this.getGachaType()) .setScheduleId(this.getScheduleId()) .setBeginTime(this.getBeginTime()) .setEndTime(this.getEndTime()) - .setCostItemId(this.getCostItem()) - .setCostItemNum(1) + .setCostItemId(costItem1.getId()) + .setCostItemNum(costItem1.getCount()) + .setTenCostItemId(costItem10.getId()) + .setTenCostItemNum(costItem10.getCount()) .setGachaPrefabPath(this.getPrefabPath()) .setGachaPreviewPrefabPath(this.getPreviewPrefabPath()) .setGachaProbUrl(details) .setGachaProbUrlOversea(details) .setGachaRecordUrl(record) .setGachaRecordUrlOversea(record) - .setTenCostItemId(this.getCostItem()) - .setTenCostItemNum(10) .setLeftGachaTimes(Integer.MAX_VALUE) .setGachaTimesLimit(Integer.MAX_VALUE) .setGachaSortId(this.getSortId()); - if (this.getTitlePath() != null) { info.setGachaTitlePath(this.getTitlePath()); } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index a0337be7f..4cbfde094 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileReader; import java.nio.file.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -46,6 +47,8 @@ public class GachaManager { private static final int starglitterId = 221; private static final int stardustId = 222; + private int[] fallbackItems4Pool2Default = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405}; + private int[] fallbackItems5Pool2Default = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}; public GachaManager(GameServer server) { this.server = server; @@ -156,16 +159,16 @@ public class GachaManager { // Simple weighted selection with an upper bound for the roll that cuts off trailing entries // All weights must be >= 0 int total = 0; - for (int i : weights) { - if (i < 0) { + for (int weight : weights) { + if (weight < 0) { throw new IllegalArgumentException("Weights must be non-negative!"); } - total += i; + total += weight; } int roll = ThreadLocalRandom.current().nextInt((total < cutoff)? total : cutoff); int subTotal = 0; - for (int i : weights) { - subTotal += i; + for (int i=0; i 0) - && (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) - || (this.randomRange(1, 100) <= banner.getEventChance(rarity))) { + boolean pullFeatured = (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) // Lost previous coinflip + || (this.randomRange(1, 100) <= banner.getEventChance(rarity)); // Won this coinflip + if (pullFeatured && (featured.length > 0)) { itemId = getRandom(featured); gachaInfo.setFailedFeaturedItemPulls(rarity, 0); } else { gachaInfo.addFailedFeaturedItemPulls(rarity, 1); if (fallback1.length < 1) { - itemId = getRandom(fallback2); // Don't ever run an empty fallback2 btw - } else { + if (fallback2.length < 1) { + itemId = getRandom((rarity==5)? fallbackItems5Pool2Default : fallbackItems4Pool2Default); + } else { + itemId = getRandom(fallback2); + } + } else if (fallback2.length < 1) { + itemId = getRandom(fallback1); + } else { // Both pools are possible, use the pool balancer int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1)); int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2)); int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly @@ -245,6 +254,7 @@ public class GachaManager { // Spend currency ItemParamData cost = banner.getCost(times); if (cost.getCount() > 0 && !inventory.payItem(cost)) { + player.sendPacket(new PacketDoGachaRsp()); return; } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java index 9144c0d8e..6d8b9ddd9 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java @@ -2,6 +2,7 @@ package emu.grasscutter.server.packet.send; import java.util.List; +import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.game.gacha.GachaBanner; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; @@ -14,16 +15,18 @@ public class PacketDoGachaRsp extends BasePacket { public PacketDoGachaRsp(GachaBanner banner, List list) { super(PacketOpcodes.DoGachaRsp); + ItemParamData costItem = banner.getCost(1); + ItemParamData costItem10 = banner.getCost(10); DoGachaRsp p = DoGachaRsp.newBuilder() .setGachaType(banner.getGachaType()) .setGachaScheduleId(banner.getScheduleId()) .setGachaTimes(list.size()) .setNewGachaRandom(12345) .setLeftGachaTimes(Integer.MAX_VALUE) - .setCostItemId(banner.getCostItem()) - .setCostItemNum(1) - .setTenCostItemId(banner.getCostItem()) - .setTenCostItemNum(10) + .setCostItemId(costItem.getId()) + .setCostItemNum(costItem.getCount()) + .setTenCostItemId(costItem10.getId()) + .setTenCostItemNum(costItem10.getCount()) .addAllGachaItemList(list) .build(); From 23d1553acafb458fd76a79d1ce3e047232c929e3 Mon Sep 17 00:00:00 2001 From: AnimeGitB Date: Thu, 12 May 2022 15:00:40 +0930 Subject: [PATCH 315/434] Fix gachadetails --- .../dispatch/http/GachaDetailsHandler.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java index 5e1877b9b..e5359a9da 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java +++ b/src/main/java/emu/grasscutter/server/dispatch/http/GachaDetailsHandler.java @@ -62,28 +62,24 @@ public final class GachaDetailsHandler implements HttpContextHandler { // Add 5-star items. Set fiveStarItems = new LinkedHashSet<>(); - Arrays.stream(banner.getRateUpItems1()).forEach(i -> fiveStarItems.add(Integer.toString(i))); - if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.EVENT) { - Arrays.stream(manager.getYellowAvatars()).forEach(i -> fiveStarItems.add(Integer.toString(i))); - } - if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.WEAPON) { - Arrays.stream(manager.getYellowWeapons()).forEach(i -> fiveStarItems.add(Integer.toString(i))); - } + Arrays.stream(banner.getRateUpItems5()).forEach(i -> fiveStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getFallbackItems5Pool1()).forEach(i -> fiveStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getFallbackItems5Pool2()).forEach(i -> fiveStarItems.add(Integer.toString(i))); response = response.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]"); // Add 4-star items. Set fourStarItems = new LinkedHashSet<>(); - Arrays.stream(banner.getRateUpItems2()).forEach(i -> fourStarItems.add(Integer.toString(i))); - Arrays.stream(manager.getPurpleAvatars()).forEach(i -> fourStarItems.add(Integer.toString(i))); - Arrays.stream(manager.getPurpleWeapons()).forEach(i -> fourStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getRateUpItems4()).forEach(i -> fourStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getFallbackItems4Pool1()).forEach(i -> fourStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getFallbackItems4Pool2()).forEach(i -> fourStarItems.add(Integer.toString(i))); response = response.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]"); // Add 3-star items. Set threeStarItems = new LinkedHashSet<>(); - Arrays.stream(manager.getBlueWeapons()).forEach(i -> threeStarItems.add(Integer.toString(i))); + Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i))); response = response.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]"); // Done. From 5b9548c63a5cbea3dd4dd673a217afc5977a26a4 Mon Sep 17 00:00:00 2001 From: kyoko12 Date: Fri, 13 May 2022 14:10:02 +0200 Subject: [PATCH 316/434] Don't silently delete config.json if there is an error. --- .../java/emu/grasscutter/Grasscutter.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 2af2415ab..af8a759a5 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -153,16 +153,22 @@ public final class Grasscutter { * Attempts to load the configuration from a file. */ public static void loadConfig() { + // Check if config.json exists. If not, we generate a new config. + if (!configFile.exists()) { + getLogger().info("config.json could not be found. Generating a default configuration ..."); + config = new ConfigContainer(); + Grasscutter.saveConfig(config); + return; + } + + // If the file already exists, we attempt to load it. try (FileReader file = new FileReader(configFile)) { config = gson.fromJson(file, ConfigContainer.class); - } catch (Exception exception) { - Grasscutter.saveConfig(null); - config = new ConfigContainer(); - } catch (Error error) { - // Occurred probably from an outdated config file. - Grasscutter.saveConfig(null); - config = new ConfigContainer(); - } + } + catch (Exception exception) { + getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); + System.exit(1); + } } public static void loadLanguage() { From f00c5d9f295c698eac79e5ad895248b61ca68b5d Mon Sep 17 00:00:00 2001 From: Kimi <34180607+Kimi898246@users.noreply.github.com> Date: Fri, 13 May 2022 20:10:40 +0800 Subject: [PATCH 317/434] Traditional Chinese | Translation Patches --- src/main/resources/languages/zh-TW.json | 146 ++++++++++++++++-------- 1 file changed, 98 insertions(+), 48 deletions(-) diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index f6ae52c9d..2b6fe34ff 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -16,6 +16,9 @@ "no_keystore_error": "[Dispatch] 未找到 SSL 憑證!已後降到 HTTP 伺服器。", "default_password": "[Dispatch] 默認的 keystore 密碼加載成功。請考慮將 config.json 的憑證密碼設定成 123456。" }, + "authentication": { + "default_unable_to_verify": "[驗證系統] 稱為 verifyUser 方法的東西在默認身份驗證程序中不可用。" + }, "no_commands_error": "此指令不適用於Dispatch-only模式。", "unhandled_request_error": "[Dispatch] 潛在的未處理請求 %s 請求:%s", "account": { @@ -46,7 +49,7 @@ "run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...", "create_resources": "正在建立 resources 資料夾...", "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。", - "version": "Grasscutter版本: %s-%s" + "version": "Grasscutter版本: %s-%s" } }, "commands": { @@ -57,6 +60,7 @@ "console_execute_error": "此指令只能在伺服器的命令提示字元執行。", "player_execute_error": "請在遊戲裡使用這條指令。", "command_exist_error": "找不到指令。", + "no_description_specified": "没有指定說明。", "invalid": { "amount": "無效的數量。", "artifactId": "無效的聖遺物ID。", @@ -96,17 +100,20 @@ "create": "已建立帳號,UID 為 %s 。", "delete": "帳號已刪除。", "no_account": "帳號不存在。", - "command_usage": "用法:account [uid]" + "command_usage": "用法:account [uid]", + "description": "建立或刪除帳號。" }, "broadcast": { "command_usage": "用法:broadcast ", - "message_sent": "公告已發送。" + "message_sent": "公告已發送。", + "description": "向所有玩家發送公告。" }, "changescene": { "usage": "用法:changescene ", "already_in_scene": "你已經在這個場景中了。", "success": "已切換至場景 %s.", - "exists_error": "此場景不存在。" + "exists_error": "此場景不存在。", + "description": "切換指定場景。" }, "clear": { "command_usage": "用法: clear ", @@ -116,35 +123,41 @@ "furniture": "已將 %s 的塵歌壺家具清空。", "displays": "已清除 %s 的顯示。", "virtuals": "已將 %s 的所有貨幣和經驗值清空。", - "everything": "已將 %s 的所有物品清空。" + "everything": "已將 %s 的所有物品清空。", + "description": "從你的背包中刪除所有未裝備且未上鎖的物品,包括稀有物品。" }, "coop": { "usage": "用法:coop ", - "success": "召喚了 %s 到 %s 的世界。" + "success": "召喚了 %s 到 %s 的世界。", + "description": "強制傳送指定用戶到他人的世界。" }, "enter_dungeon": { "usage": "用法:enterdungeon ", - "changed": "已進入副本 %s", - "not_found_error": "此副本不存在。", - "in_dungeon_error": "你已經在祕境中了。" + "changed": "已進入祕境 %s", + "not_found_error": "此祕境不存在。", + "in_dungeon_error": "你已經在祕境中了。", + "description": "進入指定祕境。" }, "giveAll": { "usage": "用法:giveall [player] [amount]", "started": "正在賦予全部物品...", "success": "已賦予全部物品。", - "invalid_amount_or_playerId": "無效的數量/玩家ID。" + "invalid_amount_or_playerId": "無效的數量/玩家ID。", + "description": "賦予所有物品。" }, "giveArtifact": { "usage": "用法:giveart|gart [player] [[,]]... [level]", "id_error": "無效的聖遺物ID。", - "success": "已把 %s 給予 %s。" + "success": "已把 %s 給予 %s。", + "description": "給予指定聖遺物。" }, "giveChar": { "usage": "用法:givechar [amount]", "given": "已將 %s 等級 %s 給予 %s。", "invalid_avatar_id": "無效的角色ID。", "invalid_avatar_level": "無效的角色等級。.", - "invalid_avatar_or_player_id": "無效的角色ID/玩家ID。" + "invalid_avatar_or_player_id": "無效的角色ID/玩家ID。", + "description": "給予指定角色。" }, "give": { "usage": "用法:give [amount] [level]", @@ -152,29 +165,42 @@ "refinement_must_between_1_and_5": "精煉度必需在 1 到 5 之間。", "given": "已經將 %s 個 %s 給予 %s。", "given_with_level_and_refinement": "已將 %s [等級%s, 精煉%s] %s個給予 %s", - "given_level": "已將 %s 等級 %s %s 個給予 %s" + "given_level": "已將 %s 等級 %s %s 個給予 %s", + "description": "給予指定物品。" }, "godmode": { - "success": "上帝模式設定為 %s 。 [用戶:%s]" + "success": "上帝模式設定為 %s 。 [用戶:%s]", + "description": "防止你受到傷害。" }, "heal": { - "success": "所有角色已被治療。" + "success": "所有角色已被治療。", + "description": "治療當前隊伍的角色。" }, "kick": { "player_kick_player": "玩家 [%s:%s] 已把 [%s:%s] 踢出", - "server_kick_player": "正在踢出玩家 [%s:%s]" + "server_kick_player": "正在踢出玩家 [%s:%s]", + "description": "從伺服器內踢出指定玩家。" }, "kill": { "usage": "用法:killall [playerUid] [sceneId]", "scene_not_found_in_player_world": "未在玩家世界中找到此場景", - "kill_monsters_in_scene": "已殺死 %s 個怪物。 [場景ID: %s]" + "kill_monsters_in_scene": "已殺死 %s 個怪物。 [場景ID: %s]", + "description": "殺死所有怪物。" }, "killCharacter": { "usage": "用法:/killcharacter [playerId]", - "success": "已殺死 %s 目前的場上角色。" + "success": "已殺死 %s 目前的場上角色。", + "description": "殺死玩家目前使用的場上角色。" + }, + "language": { + "current_language": "當前語言是: %s", + "language_changed": "語言切換至: %s", + "language_not_found": "目前客戶端沒有這種語言: %s", + "description": "顯示或切換當前語言。" }, "list": { - "success": "目前總線上人數:%s" + "success": "目前總線上人數:%s" , + "description": "查看所有在線玩家" }, "permission": { "usage": "用法:permission ", @@ -182,21 +208,26 @@ "has_error": "此玩家已擁有權限!", "remove": "權限已移除。", "not_have_error": "此玩家未擁有權限!", - "account_error": "The account cannot be found." + "account_error": "帳號不存在。", + "description": "指派或移除指定玩家的權限。" }, "position": { - "success": "坐標:%s, %s, %s\n場景ID:%s" + "success": "座標:%s, %s, %s\n場景ID:%s", + "description": "獲取目前所在位置的座標。" }, "reload": { "reload_start": "正在重新加載設定檔。", - "reload_done": "重新加載已完成。" + "reload_done": "重新加載已完成。", + "description": "重新加載設定檔和數據。" }, "resetConst": { "reset_all": "重設所有角色的命座。", - "success": "已重設 %s 的命座,重新登入後將會生效。" + "success": "已重設 %s 的命座,重新登入後將會生效。", + "description": "重置當前角色的命之座,重新登入後將會生效。" }, "resetShopLimit": { - "usage": "用法:/resetshop " + "usage": "用法:/resetshop ", + "description": "重置所選玩家的商店刷新時間。" }, "sendMail": { "usage": "用法:give [player] [amount]", @@ -218,17 +249,20 @@ "message": "<正文>", "sender": "<寄件者>", "arguments": " [數量] [等級]", - "error": "錯誤:無效的編寫階段 %s。需要 stacktrace 請查看伺服器命令提示字元。" + "error": "錯誤:無效的編寫階段 %s。需要 stacktrace 請查看伺服器命令提示字元。", + "description": "向指定用戶發送郵件。此指令的用法可根據附加的參數而改變。" }, "sendMessage": { "usage": "用法:sendmessage ", - "success": "訊息已發送。" + "success": "訊息已發送。", + "description": "向指定玩家發送訊息。" }, "setFetterLevel": { "usage": "用法:setfetterlevel ", "range_error": "好感度必須在 0 到 10 之間。", "success": "好感等級已設定為 %s", - "level_error": "無效的好感度。" + "level_error": "無效的好感度。", + "description": "設定當前角色的好感度等級。" }, "setStats": { "usage_console": "用法:setstats|stats @ ", @@ -239,77 +273,93 @@ "player_error": "玩家不存在或已離線。", "set_self": "%s 已經設為 %s。", "set_for_uid": "%s 的使用者 %s 更改為 %s。", - "set_max_hp": "最大生命值更改為 %s。" + "set_max_hp": "最大生命值更改為 %s。", + "description": "設定當前角色的數據類型。" }, "setWorldLevel": { "usage": "用法:setworldlevel ", "value_error": "世界等級必須設定在0-8之間。", "success": "已將世界等級設為%s。", - "invalid_world_level": "無效的世界等級。" + "invalid_world_level": "無效的世界等級。", + "description": "設定世界等級,執行指令後需重新登入後才會生效。" }, "spawn": { "usage": "用法:spawn [amount] [level(僅限怪物)]", - "success": "已生成 %s 個 %s。" + "success": "已生成 %s 個 %s。", + "description": "在你附近生成一個實體動物。" }, "stop": { - "success": "正在關閉伺服器..." + "success": "正在關閉伺服器...", + "description": "以正常的方式關閉伺服器。" }, "talent": { "usage_1": "設定天賦等級:/talent set ", "usage_2": "另一種設定天賦等級的指令使用方法:/talent ", "usage_3": "獲取天賦ID指令用法:/talent getid", - "lower_16": "無效的技能等級,技能等級應低於 16。", + "lower_16": "無效的天賦等級,技能等級應低於 16。", "set_id": "將天賦等級設為%s。", "set_atk": "將普通攻擊等級設為 %s。", - "set_e": "設定天賦E等級至 %s。", - "set_q": "設定天賦Q等級至 %s。", + "set_e": "設定元素戰技的天賦等級至 %s。", + "set_q": "設定元素爆發的天賦等級至 %s。", "invalid_skill_id": "無效的技能ID。", "set_this": "將天賦等級設為 %s。", "invalid_level": "無效的天賦等級。", "normal_attack_id": "普通攻擊的 ID 為 %s。", - "e_skill_id": "E技能ID %s。", - "q_skill_id": "Q技能ID %s。" + "e_skill_id": "元素戰技技能ID %s。", + "q_skill_id": "元素爆發技能ID %s。", + "description": "設定當前角色的天賦等級" }, "teleportAll": { "success": "召喚了所有玩家到你的位置上。", - "error": "此指令僅可在多人遊戲下可用。" + "error": "此指令僅可在多人遊戲下可用。", + "description": "將你世界裡的所有玩家傳送到你目前的所在位置。" }, "teleport": { "usage_server": "用法:/tp @ [scene id]", "usage": "用法:/tp [@] [scene id]", "specify_player_id": "你必須指定一個玩家ID。", - "invalid_position": "無效的位置。", - "success": "傳送 %s 到坐標 %s,%s,%s ,場景為 %s" + "invalid_position": "無效的座標。", + "success": "傳送 %s 到座標 %s,%s,%s ,場景為 %s 。", + "description": "將玩家的位置傳送到你所指定的座標。" + }, + "tower": { + "unlock_done": "解鎖所有級別的深境螺旋已全部解鎖。" }, "weather": { "usage": "用法:weather [climateId]", "success": "已將當前天氣設定為 %s ,氣候則為 %s 。", - "invalid_id": "無效的ID。" + "invalid_id": "無效的ID。", + "description": "更改目前的天氣。" }, "drop": { "command_usage": "用法:drop [amount]", - "success": "已將 %s x %s 丟在附近。" + "success": "已將 %s x %s 丟在附近。", + "description": "在你附近丟下一個物品。" }, "help": { "usage": "用法:", "aliases": "別名:", + "description": "發送幫助信息或顯示特定命令的信息", "available_commands": "可用指令:" }, + "restart": { + "description": "重新啟動伺服器。" + }, "unlocktower": { "success": "解鎖完成。", - "description": "解鎖所有級別的深境螺旋" + "description": "解鎖所有級別的深境螺旋。" }, "resetshop": { - "description": "重置商店時間" + "description": "重置商店刷新時間。" } }, "gacha": { "details": { - "title": "Banner Details", - "available_five_stars": "Available 5-star Items", - "available_four_stars": "Available 4-star Items", - "available_three_stars": "Available 3-star Items", - "template_missing": "data/gacha_details.html is missing." + "title": "祈願詳情", + "available_five_stars": "可獲得的5星物品", + "available_four_stars": "可獲得的4星物品", + "available_three_stars": "可獲得的3星物品", + "template_missing": "data/gacha_details.html 不存在。" } } } From 00ffbea4513c0bc2f7ec72339c514a94443eb812 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 13 May 2022 06:24:50 -0700 Subject: [PATCH 318/434] Fixed quests not finishing their questline --- .../java/emu/grasscutter/data/def/QuestData.java | 11 +++++++++++ .../emu/grasscutter/game/quest/GameMainQuest.java | 1 + .../java/emu/grasscutter/game/quest/GameQuest.java | 13 ++++++++++--- .../emu/grasscutter/game/quest/QuestManager.java | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/emu/grasscutter/data/def/QuestData.java b/src/main/java/emu/grasscutter/data/def/QuestData.java index 31ac2ce7e..13e806dab 100644 --- a/src/main/java/emu/grasscutter/data/def/QuestData.java +++ b/src/main/java/emu/grasscutter/data/def/QuestData.java @@ -15,6 +15,9 @@ public class QuestData extends GameResource { private int Order; private long DescTextMapHash; + private boolean FinishParent; + private boolean IsRewind; + private LogicType AcceptCondComb; private QuestCondition[] acceptConditons; private LogicType FinishCondComb; @@ -45,6 +48,14 @@ public class QuestData extends GameResource { return DescTextMapHash; } + public boolean finishParent() { + return FinishParent; + } + + public boolean isRewind() { + return IsRewind; + } + public LogicType getAcceptCondComb() { return AcceptCondComb; } diff --git a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java index bf88b8efe..613819d0b 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java @@ -91,6 +91,7 @@ public class GameMainQuest { this.isFinished = true; this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED; this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this)); + this.save(); } public void save() { diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java index b242166eb..5e1126fcb 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -143,21 +143,28 @@ public class GameQuest { this.getOwner().getSession().send(new PacketQuestProgressUpdateNotify(this)); this.getOwner().getSession().send(new PacketQuestListUpdateNotify(this)); - this.save(); - this.tryAcceptQuestLine(); + if (this.getData().finishParent()) { + // This quest finishes the questline - the main quest will also save the quest to db so we dont have to call save() here + this.getMainQuest().finish(); + } else { + // Try and accept other quests if possible + this.tryAcceptQuestLine(); + this.save(); + } } public boolean tryAcceptQuestLine() { try { MainQuestData questConfig = GameData.getMainQuestDataMap().get(this.getMainQuestId()); + for (SubQuestData subQuest : questConfig.getSubQuests()) { GameQuest quest = getMainQuest().getChildQuestById(subQuest.getSubId()); if (quest == null) { QuestData questData = GameData.getQuestDataMap().get(subQuest.getSubId()); - if (questData == null) { + if (questData == null || questData.getAcceptCond() == null) { continue; } diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index 548e8241a..745ce9ef8 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -128,7 +128,7 @@ public class QuestManager { QuestData data = quest.getData(); for (int i = 0; i < data.getFinishCond().length; i++) { - if (quest.getFinishProgressList()[i] == 1) { + if (quest.getFinishProgressList() == null || quest.getFinishProgressList()[i] == 1) { continue; } From 0ffaba44eaf34faa7978cd2ffc94281844b5a2dc Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 13 May 2022 06:33:12 -0700 Subject: [PATCH 319/434] Implement QUEST_CONTENT_FINISH_PLOT --- .../game/quest/content/ContentFinishPlot.java | 17 +++++++++++++++++ .../server/packet/recv/HandlerNpcTalkReq.java | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 src/main/java/emu/grasscutter/game/quest/content/ContentFinishPlot.java diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentFinishPlot.java b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishPlot.java new file mode 100644 index 000000000..d8e0cd4e5 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishPlot.java @@ -0,0 +1,17 @@ +package emu.grasscutter.game.quest.content; + +import emu.grasscutter.data.def.QuestData.QuestCondition; +import emu.grasscutter.game.quest.QuestValue; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.enums.QuestTrigger; +import emu.grasscutter.game.quest.handlers.QuestBaseHandler; + +@QuestValue(QuestTrigger.QUEST_CONTENT_FINISH_PLOT) +public class ContentFinishPlot extends QuestBaseHandler { + + @Override + public boolean execute(GameQuest quest, QuestCondition condition, int... params) { + return condition.getParam()[0] == params[0]; + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java index 82248c98c..3dae7fe10 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java @@ -16,7 +16,9 @@ public class HandlerNpcTalkReq extends PacketHandler { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { NpcTalkReq req = NpcTalkReq.parseFrom(payload); + // Why are there 2 quest triggers that do the same thing... session.getPlayer().getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId()); + session.getPlayer().getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_FINISH_PLOT, req.getTalkId()); session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId())); } From f1a64d07cf3c851e5d6740cd71c9c74020292d93 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:37:17 -0400 Subject: [PATCH 320/434] Add `lombok` --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 4434ed28e..2074ec553 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,9 @@ dependencies { implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1' protobuf files('proto/') + + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' } configurations.all { From 5d557e652520ad22104703dfff3eecf35dbe1533 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:37:49 -0400 Subject: [PATCH 321/434] Update project to `1.1.2-dev` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2074ec553..68f6d449a 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' -version = '1.1.1-dev' +version = '1.1.2-dev' sourceCompatibility = 17 targetCompatibility = 17 From f16c3fb8bc527255626c7984c300299eab083260 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:38:17 -0400 Subject: [PATCH 322/434] Add new authentication system --- .../auth/AuthenticationSystem.java | 101 +++++++++++ .../emu/grasscutter/auth/Authenticator.java | 17 ++ .../auth/DefaultAuthentication.java | 40 +++++ .../auth/DefaultAuthenticators.java | 161 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 src/main/java/emu/grasscutter/auth/AuthenticationSystem.java create mode 100644 src/main/java/emu/grasscutter/auth/Authenticator.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthentication.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java new file mode 100644 index 000000000..dae3402f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -0,0 +1,101 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; +import express.http.Request; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import javax.annotation.Nullable; + +/** + * Defines an authenticator for the server. + * Can be changed by plugins. + */ +public interface AuthenticationSystem { + + /** + * Called when a user requests to make an account. + * @param username The provided username. + * @param password The provided password. (SHA-256'ed) + */ + void createAccount(String username, String password); + + /** + * Called when a user requests to reset their password. + * @param username The username of the account to reset. + */ + void resetPassword(String username); + + /** + * This is the authenticator used for password authentication. + * @return An authenticator. + */ + Authenticator getPasswordAuthenticator(); + + /** + * This is the authenticator used for token authentication. + * @return An authenticator. + */ + Authenticator getTokenAuthenticator(); + + /** + * This is the authenticator used for session authentication. + * @return An authenticator. + */ + Authenticator getSessionKeyAuthenticator(); + + /** + * A data container that holds relevant data for authenticating a client. + * Call {@link AuthenticationRequest#builder()} to create a builder. + */ + @Builder @AllArgsConstructor @Getter + class AuthenticationRequest { + private final Request request; + @Nullable private final LoginAccountRequestJson passwordRequest; + @Nullable private final LoginTokenRequestJson tokenRequest; + @Nullable private final ComboTokenReqJson sessionKeyRequest; + @Nullable private final ComboTokenReqJson.LoginTokenData sessionKeyData; + } + + /** + * Generates an authentication request from a {@link LoginAccountRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromPasswordRequest(Request request, LoginAccountRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .passwordRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link LoginTokenRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromTokenRequest(Request request, LoginTokenRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .tokenRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link ComboTokenReqJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromComboTokenRequest(Request request, ComboTokenReqJson jsonData, + ComboTokenReqJson.LoginTokenData tokenData) { + return AuthenticationRequest.builder() + .request(request) + .sessionKeyRequest(jsonData) + .sessionKeyData(tokenData) + .build(); + } +} diff --git a/src/main/java/emu/grasscutter/auth/Authenticator.java b/src/main/java/emu/grasscutter/auth/Authenticator.java new file mode 100644 index 000000000..a5d756d8c --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/Authenticator.java @@ -0,0 +1,17 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; + +/** + * Handles username/password authentication from the client. + * @param The response object type. Should be {@link LoginResultJson} or {@link ComboTokenResJson} + */ +public interface Authenticator { + + /** + * Attempt to authenticate the client with the provided credentials. + * @param request The authentication request wrapped in a {@link AuthenticationSystem.AuthenticationRequest} object. + * @return The result of the login in an object. + */ + T authenticate(AuthenticationSystem.AuthenticationRequest request); +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java new file mode 100644 index 000000000..2864b80b5 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -0,0 +1,40 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.auth.DefaultAuthenticators.*; +import emu.grasscutter.server.http.objects.ComboTokenResJson; +import emu.grasscutter.server.http.objects.LoginResultJson; + +/** + * The default Grasscutter authentication implementation. + * Allows all users to access any account. + */ +public final class DefaultAuthentication implements AuthenticationSystem { + private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); + private final Authenticator tokenAuthenticator = new TokenAuthenticator(); + private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + + @Override + public void createAccount(String username, String password) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public void resetPassword(String username) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public Authenticator getPasswordAuthenticator() { + return this.passwordAuthenticator; + } + + @Override + public Authenticator getTokenAuthenticator() { + return this.tokenAuthenticator; + } + + @Override + public Authenticator getSessionKeyAuthenticator() { + return this.sessionKeyAuthenticator; + } +} diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java new file mode 100644 index 000000000..298d24493 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -0,0 +1,161 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.server.http.objects.*; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.utils.Language.translate; + +/** + * A class containing default authenticators. + */ +public final class DefaultAuthenticators { + + /** + * Handles the authentication request from the username & password form. + */ + public static class PasswordAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getPasswordRequest(); + assert requestData != null; // This should never be null. + + boolean successfulLogin = false; + String address = request.getRequest().ip(); + String responseMessage = translate("messages.dispatch.account.username_error"); + + // Get account from database. + Account account = DatabaseHelper.getAccountByName(requestData.account); + + // Check if account exists. + if(account == null && ACCOUNT.autoCreate) { + // This account has been created AUTOMATICALLY. There will be no permissions added. + account = DatabaseHelper.createAccountWithId(requestData.account, 0); + + // Check if the account was created successfully. + if(account == null) { + responseMessage = translate("messages.dispatch.account.username_create_error"); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address)); + } else { + // Add default permissions. + for (var permission : ACCOUNT.defaultPermissions) + account.addPermission(permission); + + // Continue with login. + successfulLogin = true; + + // Log the creation. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); + } + } else if(account != null) + successfulLogin = true; + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.generateSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", address, account.getId())); + } else { + response.retcode = -201; + response.message = responseMessage; + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a registry token. + */ + public static class TokenAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getTokenRequest(); + assert requestData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Log the attempt. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", address)); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(requestData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(requestData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.getSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", address, requestData.uid)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.account_cache_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a combo token/session key. + */ + public static class SessionKeyAuthenticator implements Authenticator { + @Override public ComboTokenResJson authenticate(AuthenticationRequest request) { + var response = new ComboTokenResJson(); + + var requestData = request.getSessionKeyRequest(); + var loginData = request.getSessionKeyData(); + assert requestData != null; assert loginData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(loginData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(loginData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.open_id = account.getId(); + response.data.combo_id = "157795300"; + response.data.combo_token = account.generateLoginToken(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", address)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.session_key_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", address)); + } + + return response; + } + } +} From 3425e0f1fca8f03bcac50a141f23822c75aa0cf5 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:38:54 -0400 Subject: [PATCH 323/434] Add JSON-related methods to `Utils.java` --- .../java/emu/grasscutter/utils/Utils.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 4af62bfb4..1fe026bd8 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -6,10 +6,7 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.*; import java.time.temporal.TemporalAdjusters; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.Locale; +import java.util.*; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; @@ -308,10 +305,42 @@ public final class Utils { } /** - * get language code from Locale + * Gets the language code from a given locale. + * @param locale A locale. + * @return A string in the format of 'XX-XX'. */ public static String getLanguageCode(Locale locale) { return String.format("%s-%s", locale.getLanguage(), locale.getCountry()); } + /** + * Base64 encodes a given byte array. + * @param toEncode An array of bytes. + * @return A base64 encoded string. + */ + public static String base64Encode(byte[] toEncode) { + return Base64.getEncoder().encodeToString(toEncode); + } + + /** + * Base64 decodes a given string. + * @param toDecode A base64 encoded string. + * @return An array of bytes. + */ + public static byte[] base64Decode(String toDecode) { + return Base64.getDecoder().decode(toDecode); + } + + /** + * Safely JSON decodes a given string. + * @param jsonData The JSON-encoded data. + * @return JSON decoded data, or null if an exception occurred. + */ + public static T jsonDecode(String jsonData, Class classType) { + try { + return Grasscutter.getGsonFactory().fromJson(jsonData, classType); + } catch (Exception ignored) { + return null; + } + } } From e08a9c0d709399482d30a6e65f3271e786c15e27 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:39:40 -0400 Subject: [PATCH 324/434] Refactor dispatch (now called HTTP) server (pt. 1) --- .../java/emu/grasscutter/Configuration.java | 7 +- .../java/emu/grasscutter/Grasscutter.java | 150 +++++++++----- .../server/dispatch/DispatchServer.java | 20 +- .../grasscutter/server/http/HttpServer.java | 176 +++++++++++++++++ .../emu/grasscutter/server/http/Router.java | 16 ++ .../server/http/dispatch/DispatchHandler.java | 100 ++++++++++ .../server/http/dispatch/RegionHandler.java | 186 ++++++++++++++++++ .../http/handlers/AnnouncementsHandler.java | 58 ++++++ .../server/http/handlers/GenericHandler.java | 47 +++++ .../server/http/handlers/LogHandler.java | 18 ++ .../http/objects/ComboTokenReqJson.java | 15 ++ .../http/objects/ComboTokenResJson.java | 17 ++ .../http/objects/LoginAccountRequestJson.java | 7 + .../server/http/objects/LoginResultJson.java | 38 ++++ .../http/objects/LoginTokenRequestJson.java | 6 + .../grasscutter/utils/ConfigContainer.java | 31 ++- src/main/resources/languages/en-US.json | 3 +- 17 files changed, 828 insertions(+), 67 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/http/HttpServer.java create mode 100644 src/main/java/emu/grasscutter/server/http/Router.java create mode 100644 src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 7adc334c1..52bfa65aa 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -34,11 +34,12 @@ public final class Configuration extends ConfigContainer { public static final Database DATABASE = config.databaseInfo; public static final Account ACCOUNT = config.account; - public static final Dispatch DISPATCH_INFO = config.server.dispatch; + public static final HTTP HTTP_INFO = config.server.http; public static final Game GAME_INFO = config.server.game; + public static final Dispatch DISPATCH_INFO = config.server.dispatch; - public static final Encryption DISPATCH_ENCRYPTION = config.server.dispatch.encryption; - public static final Policies DISPATCH_POLICIES = config.server.dispatch.policies; + public static final Encryption HTTP_ENCRYPTION = config.server.http.encryption; + public static final Policies HTTP_POLICIES = config.server.http.policies; public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions; public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 73e761e6e..bddfa9964 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -3,10 +3,18 @@ package emu.grasscutter; import java.io.*; import java.util.Calendar; +import emu.grasscutter.auth.AuthenticationSystem; +import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.command.CommandMap; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.server.http.HttpServer; +import emu.grasscutter.server.http.dispatch.DispatchHandler; +import emu.grasscutter.server.http.handlers.AnnouncementsHandler; +import emu.grasscutter.server.http.handlers.GenericHandler; +import emu.grasscutter.server.http.handlers.LogHandler; +import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; import org.jline.reader.EndOfFileException; @@ -47,8 +55,10 @@ public final class Grasscutter { private static int day; // Current day of week. private static DispatchServer dispatchServer; + private static HttpServer httpServer; private static GameServer gameServer; private static PluginManager pluginManager; + private static AuthenticationSystem authenticationSystem; public static final Reflections reflector = new Reflections("emu.grasscutter"); public static ConfigContainer config; @@ -98,14 +108,27 @@ public final class Grasscutter { // Initialize database. DatabaseManager.initialize(); + + // Initialize the default authentication system. + authenticationSystem = new DefaultAuthentication(); // Create server instances. dispatchServer = new DispatchServer(); + httpServer = new HttpServer(); gameServer = new GameServer(); // Create a server hook instance with both servers. new ServerHook(gameServer, dispatchServer); + // Create plugin manager instance. pluginManager = new PluginManager(); + // Add HTTP routes after loading plugins. + httpServer.addRouter(HttpServer.UnhandledRequestRouter.class); + httpServer.addRouter(HttpServer.DefaultRequestRouter.class); + httpServer.addRouter(RegionHandler.class); + httpServer.addRouter(LogHandler.class); + httpServer.addRouter(GenericHandler.class); + httpServer.addRouter(AnnouncementsHandler.class); + httpServer.addRouter(DispatchHandler.class); // Start servers. var runMode = SERVER.runMode; @@ -114,6 +137,7 @@ public final class Grasscutter { gameServer.start(); } else if (runMode == ServerRunMode.DISPATCH_ONLY) { dispatchServer.start(); + httpServer.start(); } else if (runMode == ServerRunMode.GAME_ONLY) { gameServer.start(); } else { @@ -141,6 +165,19 @@ public final class Grasscutter { pluginManager.disablePlugins(); } + /* + * Methods for the language system component. + */ + + public static void loadLanguage() { + var locale = config.language.language; + language = Language.getLanguage(Utils.getLanguageCode(locale)); + } + + /* + * Methods for the configuration system component. + */ + /** * Attempts to load the configuration from a file. */ @@ -157,11 +194,6 @@ public final class Grasscutter { } } - public static void loadLanguage() { - var locale = config.language.language; - language = Language.getLanguage(Utils.getLanguageCode(locale)); - } - /** * Saves the provided server configuration. * @param config The configuration to save, or null for a new one. @@ -178,44 +210,10 @@ public final class Grasscutter { } } - public static void startConsole() { - // Console should not start in dispatch only mode. - if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { - getLogger().info(translate("messages.dispatch.no_commands_error")); - return; - } - - getLogger().info(translate("messages.status.done")); - String input = null; - boolean isLastInterrupted = false; - while (true) { - try { - input = consoleLineReader.readLine("> "); - } catch (UserInterruptException e) { - if (!isLastInterrupted) { - isLastInterrupted = true; - Grasscutter.getLogger().info("Press Ctrl-C again to shutdown."); - continue; - } else { - Runtime.getRuntime().exit(0); - } - } catch (EndOfFileException e) { - Grasscutter.getLogger().info("EOF detected."); - continue; - } catch (IOError e) { - Grasscutter.getLogger().error("An IO error occurred.", e); - continue; - } - - isLastInterrupted = false; - try { - CommandMap.getInstance().invoke(null, null, input); - } catch (Exception e) { - Grasscutter.getLogger().error(translate("messages.game.command_error"), e); - } - } - } - + /* + * Getters for the various server components. + */ + public static ConfigContainer getConfig() { return config; } @@ -271,16 +269,74 @@ public final class Grasscutter { public static PluginManager getPluginManager() { return pluginManager; } - - public static void updateDayOfWeek() { - Calendar calendar = Calendar.getInstance(); - day = calendar.get(Calendar.DAY_OF_WEEK); + + public static AuthenticationSystem getAuthenticationSystem() { + return authenticationSystem; } public static int getCurrentDayOfWeek() { return day; } + + /* + * Utility methods. + */ + + public static void updateDayOfWeek() { + Calendar calendar = Calendar.getInstance(); + day = calendar.get(Calendar.DAY_OF_WEEK); + } + public static void startConsole() { + // Console should not start in dispatch only mode. + if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { + getLogger().info(translate("messages.dispatch.no_commands_error")); + return; + } + + getLogger().info(translate("messages.status.done")); + String input = null; + boolean isLastInterrupted = false; + while (true) { + try { + input = consoleLineReader.readLine("> "); + } catch (UserInterruptException e) { + if (!isLastInterrupted) { + isLastInterrupted = true; + Grasscutter.getLogger().info("Press Ctrl-C again to shutdown."); + continue; + } else { + Runtime.getRuntime().exit(0); + } + } catch (EndOfFileException e) { + Grasscutter.getLogger().info("EOF detected."); + continue; + } catch (IOError e) { + Grasscutter.getLogger().error("An IO error occurred.", e); + continue; + } + + isLastInterrupted = false; + try { + CommandMap.getInstance().invoke(null, null, input); + } catch (Exception e) { + Grasscutter.getLogger().error(translate("messages.game.command_error"), e); + } + } + } + + /** + * Sets the authentication system for the server. + * @param authenticationSystem The authentication system to use. + */ + public static void setAuthenticationSystem(AuthenticationSystem authenticationSystem) { + Grasscutter.authenticationSystem = authenticationSystem; + } + + /* + * Enums for the configuration. + */ + public enum ServerRunMode { HYBRID, DISPATCH_ONLY, GAME_ONLY } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 4e09f8881..8b8a9a185 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -117,7 +117,7 @@ public final class DispatchServer { .setTitle(DISPATCH_INFO.defaultName) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + defaultServerName) @@ -150,7 +150,7 @@ public final class DispatchServer { .setTitle(regionInfo.Title) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + regionInfo.Name) @@ -189,14 +189,14 @@ public final class DispatchServer { Server server = new Server(); ServerConnector serverConnector; - if(DISPATCH_ENCRYPTION.useEncryption) { + if(HTTP_ENCRYPTION.useEncryption) { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - File keystoreFile = new File(DISPATCH_ENCRYPTION.keystore); + File keystoreFile = new File(HTTP_ENCRYPTION.keystore); if(keystoreFile.exists()) { try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword(DISPATCH_ENCRYPTION.keystorePassword); + sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); } catch (Exception e) { e.printStackTrace(); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); @@ -214,7 +214,7 @@ public final class DispatchServer { serverConnector = new ServerConnector(server, sslContextFactory); } else { Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); - DISPATCH_ENCRYPTION.useEncryption = false; + HTTP_ENCRYPTION.useEncryption = false; serverConnector = new ServerConnector(server); } @@ -227,18 +227,19 @@ public final class DispatchServer { return server; }); - config.enforceSsl = DISPATCH_ENCRYPTION.useEncryption; + config.enforceSsl = HTTP_ENCRYPTION.useEncryption; if(SERVER.debugLevel == ServerDebugMode.ALL) { config.enableDevLogging(); } - if (DISPATCH_POLICIES.cors.enabled) { - var corsPolicy = DISPATCH_POLICIES.cors; + if (HTTP_POLICIES.cors.enabled) { + var corsPolicy = HTTP_POLICIES.cors; if (corsPolicy.allowedOrigins.length > 0) config.enableCorsForOrigin(corsPolicy.allowedOrigins); else config.enableCorsForAllOrigins(); } }); + httpServer.get("/", (req, res) -> res.send("" + translate("messages.status.welcome") + "")); httpServer.raw().error(404, ctx -> { @@ -279,6 +280,7 @@ public final class DispatchServer { res.send(event.getRegionList()); }); + // /server/:id -> 2.6.5x httpServer.get("/query_cur_region/:id", (req, res) -> { String regionName = req.params("id"); // Log diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java new file mode 100644 index 000000000..dc0d396a6 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -0,0 +1,176 @@ +package emu.grasscutter.server.http; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import express.Express; +import io.javalin.Javalin; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import java.io.File; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.utils.Language.translate; + +/** + * Manages all HTTP-related classes. + * (including dispatch, announcements, gacha, etc.) + */ +public final class HttpServer { + private final Express express; + + /** + * Configures the Express application. + */ + public HttpServer() { + this.express = new Express(config -> { + // Set the Express HTTP server. + config.server(HttpServer::createServer); + + // Configure encryption/HTTPS/SSL. + config.enforceSsl = HTTP_ENCRYPTION.useEncryption; + + // Configure HTTP policies. + if(HTTP_POLICIES.cors.enabled) { + var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins; + if (allowedOrigins.length > 0) + config.enableCorsForOrigin(allowedOrigins); + else config.enableCorsForAllOrigins(); + } + + // Configure debug logging. + if(SERVER.debugLevel == ServerDebugMode.ALL) + config.enableDevLogging(); + + // Disable compression on static files. + config.precompressStaticFiles = false; + }); + } + + /** + * Creates an HTTP(S) server. + * @return A server instance. + */ + @SuppressWarnings("resource") + private static Server createServer() { + Server server = new Server(); + ServerConnector serverConnector + = new ServerConnector(server); + + if(HTTP_ENCRYPTION.useEncryption) { + var sslContextFactory = new SslContextFactory.Server(); + var keystoreFile = new File(HTTP_ENCRYPTION.keystore); + + if(!keystoreFile.exists()) {; + HTTP_ENCRYPTION.useEncryption = false; + HTTP_ENCRYPTION.useInRouting = false; + + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); + } else try { + sslContextFactory.setKeyStorePath(keystoreFile.getPath()); + sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); + } catch (Exception ignored) { + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); + + try { + sslContextFactory.setKeyStorePath(keystoreFile.getPath()); + sslContextFactory.setKeyStorePassword("123456"); + + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password")); + } catch (Exception exception) { + Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error"), exception); + } + } finally { + serverConnector = new ServerConnector(server, sslContextFactory); + } + } + + serverConnector.setPort(HTTP_INFO.bindPort); + server.setConnectors(new ServerConnector[]{serverConnector}); + + return server; + } + + /** + * Returns the handle for the Express application. + * @return A Javalin instance. + */ + public Javalin getHandle() { + return this.express.raw(); + } + + /** + * Initializes the provided class. + * @param router The router class. + * @return Method chaining. + */ + @SuppressWarnings("UnusedReturnValue") + public HttpServer addRouter(Class router, Object... args) { + // Get all constructor parameters. + Class[] types = new Class[args.length]; + for(var argument : args) + types[args.length - 1] = argument.getClass(); + + try { // Create a router instance & apply routes. + var constructor = router.getDeclaredConstructor(types); // Get the constructor. + var routerInstance = constructor.newInstance(args); // Create instance. + routerInstance.applyRoutes(this.express, this.getHandle()); // Apply routes. + } catch (Exception exception) { + Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception); + } return this; + } + + /** + * Starts listening on the HTTP server. + */ + public void start() { + // Attempt to start the HTTP server. + this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort); + + // Log bind information. + Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(this.express.raw().port()))); + } + + /** + * Handles the '/' (index) endpoint on the Express application. + */ + public static class DefaultRequestRouter implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/", (req, res) -> res.send(""" + + + + + + %s + + """.formatted(translate("messages.status.welcome")))); + } + } + + /** + * Handles unhandled endpoints on the Express application. + */ + public static class UnhandledRequestRouter implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + handle.error(404, context -> { + if(SERVER.debugLevel == ServerDebugMode.MISSING) + Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); + context.contentType("text/html"); + context.result(""" + + + + + + + + + + + """); + }); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/Router.java b/src/main/java/emu/grasscutter/server/http/Router.java new file mode 100644 index 000000000..1720d7ca0 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/Router.java @@ -0,0 +1,16 @@ +package emu.grasscutter.server.http; + +import express.Express; +import io.javalin.Javalin; + +/** + * Defines routes for an {@link Express} instance. + */ +public interface Router { + + /** + * Called when the router is initialized by Express. + * @param express An Express instance. + */ + void applyRoutes(Express express, Javalin handle); +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java new file mode 100644 index 000000000..22a31fe6a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -0,0 +1,100 @@ +package emu.grasscutter.server.http.dispatch; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.server.http.objects.*; +import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import static emu.grasscutter.utils.Language.translate; + +/** + * Handles requests related to authentication. (aka dispatch) + */ +public final class DispatchHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // Username & Password login (from client). + express.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin); + // Cached token login (from registry). + express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin); + // Combo token login (from session key). + express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin); + } + + /** + * @route /hk4e_global/mdk/shield/api/login + */ + private static void clientLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, LoginAccountRequestJson.class); + + // Validate body data. + if(bodyData == null) + return; + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getPasswordAuthenticator() + .authenticate(AuthenticationSystem.fromPasswordRequest(request, bodyData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } + + /** + * @route /hk4e_global/mdk/shield/api/verify + */ + private static void tokenLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, LoginTokenRequestJson.class); + + // Validate body data. + if(bodyData == null) + return; + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getTokenAuthenticator() + .authenticate(AuthenticationSystem.fromTokenRequest(request, bodyData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } + + /** + * @route /hk4e_global/combo/granter/login/v2/login + */ + private static void sessionKeyLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, ComboTokenReqJson.class); + + // Validate body data. + if(bodyData == null || bodyData.data == null) + return; + + // Decode additional body data. + var tokenData = Utils.jsonDecode(bodyData.data, LoginTokenData.class); + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getSessionKeyAuthenticator() + .authenticate(AuthenticationSystem.fromComboTokenRequest(request, bodyData, tokenData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java new file mode 100644 index 000000000..e720a4b15 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -0,0 +1,186 @@ +package emu.grasscutter.server.http.dispatch; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerRunMode; +import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*; +import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; +import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; +import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import java.io.File; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*; + +/** + * Handles requests related to region queries. + */ +public final class RegionHandler implements Router { + private String regionQuery = ""; + private String regionList = ""; + + private static final Map regions = new ConcurrentHashMap<>(); + private static String regionListResponse; + + public RegionHandler() { + try { // Read & initialize region data. + this.readRegionData(); + this.initialize(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to initialize region data.", exception); + } + } + + /** + * Loads initial region data. + */ + private void readRegionData() { + File file; + + file = new File(DATA("query_region_list.txt")); + if (file.exists()) + this.regionList = new String(FileUtils.read(file)); + else Grasscutter.getLogger().error("[Dispatch] 'query_region_list' not found!"); + + file = new File(DATA("query_cur_region.txt")); + if (file.exists()) + regionQuery = new String(FileUtils.read(file)); + else Grasscutter.getLogger().warn("[Dispatch] 'query_cur_region' not found!"); + } + + /** + * Configures region data according to configuration. + */ + private void initialize() throws InvalidProtocolBufferException { + // Decode the initial region query. + byte[] queryBase64 = Base64.getDecoder().decode(this.regionQuery); + QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(queryBase64); + + // Create regions. + List servers = new ArrayList<>(); + List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. + + var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions)); + if(SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) { + Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); + System.exit(1); + } else configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName, + lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), + lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); + + configuredRegions.forEach(region -> { + if (usedNames.contains(region.Name)) { + Grasscutter.getLogger().error("Region name already in use."); + return; + } + + // Create a region identifier. + var identifier = RegionSimpleInfo.newBuilder() + .setName(region.Name).setTitle(region.Title) + .setType("DEV_PUBLIC").setDispatchUrl( + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + + "/query_cur_region/" + region.Name) + .build(); + usedNames.add(region.Name); servers.add(identifier); + + // Create a region info object. + var regionInfo = regionQuery.getRegionInfo().toBuilder() + .setGateserverIp(region.Ip).setGateserverPort(region.Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) + .build(); + // Create an updated region query. + var updatedQuery = regionQuery.toBuilder().setRegionInfo(regionInfo).build(); + regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray()))); + }); + + // Decode the initial region list. + byte[] listBase64 = Base64.getDecoder().decode(this.regionList); + QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.parseFrom(listBase64); + + // Create an updated region list. + QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder() + .addAllRegionList(servers) + .setClientSecretKey(regionList.getClientSecretKey()) + .setClientCustomConfigEncrypted(regionList.getClientCustomConfigEncrypted()) + .setEnableLoginPc(true).build(); + + // Set the region list response. + regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); + } + + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/query_region_list", RegionHandler::queryRegionList); + express.get("/query_cur_region/:region", RegionHandler::queryCurrentRegion ); + } + + /** + * @route /query_region_list + */ + private static void queryRegionList(Request request, Response response) { + // Invoke event. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call(); + // Respond with event result. + response.send(event.getRegionList()); + + // Log to console. + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", request.ip())); + } + + /** + * @route /query_cur_region/:region + */ + private static void queryCurrentRegion(Request request, Response response) { + // Get region to query. + String regionName = request.params("region"); + + // Get region data. + String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; + if (request.query().values().size() > 0) + regionData = regions.get(regionName).getBase64(); + + // Invoke event. + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); + // Respond with event result. + response.send(event.getRegionInfo()); + + // Log to console. + Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", request.ip(), regionName)); + } + + /** + * Region data container. + */ + public static class RegionData { + private final QueryCurrRegionHttpRsp regionQuery; + private final String base64; + + public RegionData(QueryCurrRegionHttpRsp prq, String b64) { + this.regionQuery = prq; + this.base64 = b64; + } + + public QueryCurrRegionHttpRsp getRegionQuery() { + return this.regionQuery; + } + + public String getBase64() { + return this.base64; + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java new file mode 100644 index 000000000..a64e0552a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -0,0 +1,58 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Objects; + +import static emu.grasscutter.Configuration.DATA; + +/** + * Handles requests related to the announcements page. + */ +public final class AnnouncementsHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement); + // hk4e-api-os-static.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement); + // hk4e-sdk-os.hoyoverse.com + express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); + } + + private static void getAnnouncement(Request request, Response response) { + if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}"); + } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { + String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static String readToString(File file) { + long length = file.length(); + byte[] content = new byte[(int) length]; + + try { + FileInputStream in = new FileInputStream(file); + in.read(content); in.close(); + } catch (IOException ignored) { + Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); + } + + return new String(content); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java new file mode 100644 index 000000000..bb0bc8eea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java @@ -0,0 +1,47 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles all generic, hard-coded responses. + */ +public final class GenericHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // hk4e-sdk-os.hoyoverse.com + express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); + // hk4e-sdk-os.hoyoverse.com + // this could be either GET or POST based on the observation of different clients + express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); + + // api-account-os.hoyoverse.com + express.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); + + // sdk-os-static.hoyoverse.com + express.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); + // hk4e-sdk-os-static.hoyoverse.com + express.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}")); + // hk4e-sdk-os-static.hoyoverse.com + express.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}")); + // Test api? + // abtest-api-data-sg.hoyoverse.com + express.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); + + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + + // log-upload-os.mihoyo.com + express.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + // /perf/config/verify?device_id=xxx&platform=x&name=xxx + express.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); + + // webstatic-sea.hoyoverse.com + express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java new file mode 100644 index 000000000..4f52c0826 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.dispatch.ClientLogHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles logging requests made to the server. + */ +public final class LogHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // overseauspider.yuanshen.com + express.post("/log", new ClientLogHandler()); + // log-upload-os.mihoyo.com + express.post("/crash/dataUpload", new ClientLogHandler()); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java new file mode 100644 index 000000000..5642f159a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java @@ -0,0 +1,15 @@ +package emu.grasscutter.server.http.objects; + +public class ComboTokenReqJson { + public int app_id; + public int channel_id; + public String data; + public String device; + public String sign; + + public static class LoginTokenData { + public String uid; + public String token; + public boolean guest; + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java new file mode 100644 index 000000000..b592fa163 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.http.objects; + +public class ComboTokenResJson { + public String message; + public int retcode; + public LoginData data = new LoginData(); + + public static class LoginData { + public int account_type = 1; + public boolean heartbeat; + public String combo_id; + public String combo_token; + public String open_id; + public String data = "{\"guest\":false}"; + public String fatigue_remind = null; // ? + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java new file mode 100644 index 000000000..3a8193a97 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java @@ -0,0 +1,7 @@ +package emu.grasscutter.server.http.objects; + +public class LoginAccountRequestJson { + public String account; + public String password; + public boolean is_crypto; +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java new file mode 100644 index 000000000..5601c1c29 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java @@ -0,0 +1,38 @@ +package emu.grasscutter.server.http.objects; + +public class LoginResultJson { + public String message; + public int retcode; + public VerifyData data = new VerifyData(); + + public static class VerifyData { + public VerifyAccountData account = new VerifyAccountData(); + public boolean device_grant_required = false; + public String realname_operation = "NONE"; + public boolean realperson_required = false; + public boolean safe_mobile_required = false; + } + + public static class VerifyAccountData { + public String uid; + public String name = ""; + public String email = ""; + public String mobile = ""; + public String is_email_verify = "0"; + public String realname = ""; + public String identity_card = ""; + public String token; + public String safe_mobile = ""; + public String facebook_name = ""; + public String twitter_name = ""; + public String game_center_name = ""; + public String google_name = ""; + public String apple_name = ""; + public String sony_name = ""; + public String tap_name = ""; + public String country = "US"; + public String reactivate_ticket = ""; + public String area_code = "**"; + public String device_grant_ticket = ""; + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java new file mode 100644 index 000000000..d01c60401 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java @@ -0,0 +1,6 @@ +package emu.grasscutter.server.http.objects; + +public class LoginTokenRequestJson { + public String uid; + public String token; +} diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 76556700c..5a06b90be 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -96,8 +96,10 @@ public class ConfigContainer { public ServerDebugMode debugLevel = ServerDebugMode.NONE; public ServerRunMode runMode = ServerRunMode.HYBRID; - public Dispatch dispatch = new Dispatch(); + public HTTP http = new HTTP(); public Game game = new Game(); + + public Dispatch dispatch = new Dispatch(); } public static class Language { @@ -111,8 +113,8 @@ public class ConfigContainer { } /* Server options. */ - - public static class Dispatch { + + public static class HTTP { public String bindAddress = "0.0.0.0"; /* This is the address used in URLs. */ public String accessAddress = "127.0.0.1"; @@ -120,12 +122,9 @@ public class ConfigContainer { public int bindPort = 443; /* This is the port used in URLs. */ public int accessPort = 0; - + public Encryption encryption = new Encryption(); public Policies policies = new Policies(); - public Region[] regions = {}; - - public String defaultName = "Grasscutter"; } public static class Game { @@ -144,6 +143,12 @@ public class ConfigContainer { /* Data containers. */ + public static class Dispatch { + public Region[] regions = {}; + + public String defaultName = "Grasscutter"; + } + public static class Encryption { public boolean useEncryption = true; /* Should 'https' be appended to URLs? */ @@ -226,6 +231,18 @@ public class ConfigContainer { /* Objects. */ public static class Region { + public Region() { } + + public Region( + String name, String title, + String address, int port + ) { + this.Name = name; + this.Title = title; + this.Ip = address; + this.Port = port; + } + public String Name = "os_usa"; public String Title = "Grasscutter"; public String Ip = "127.0.0.1"; diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index c9c3c0c70..b23f2913d 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -33,7 +33,8 @@ "session_key_error": "Wrong session key.", "username_error": "Username not found.", "username_create_error": "Username not found, create failed." - } + }, + "router_error": "[Dispatch] Unable to attach router." }, "status": { "free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter", From 27e817a6ce6c3ab1f1de56f9baba2e3234c13277 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sat, 14 May 2022 00:03:55 +0800 Subject: [PATCH 325/434] feature(serenitea pot): Implementation of the entry function It's being perfected, so don't worry. (probably) --- proto/WidgetDoBagReq.proto | 2 +- proto/WidgetDoBagRsp.proto | 2 +- .../packet/recv/HandlerWidgetDoBagReq.java | 57 +++++++++++++++++++ .../send/PacketWidgetCoolDownNotify.java | 25 ++++++++ .../packet/send/PacketWidgetDoBagRsp.java | 28 +++++++++ .../send/PacketWidgetGadgetDataNotify.java | 26 +++++++++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerWidgetDoBagReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketWidgetCoolDownNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketWidgetDoBagRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketWidgetGadgetDataNotify.java diff --git a/proto/WidgetDoBagReq.proto b/proto/WidgetDoBagReq.proto index cbdb3460d..bc4a3403a 100644 --- a/proto/WidgetDoBagReq.proto +++ b/proto/WidgetDoBagReq.proto @@ -12,7 +12,7 @@ message WidgetDoBagReq { NONE = 0; ENET_IS_RELIABLE = 1; IS_ALLOW_CLIENT = 1; - CMD_ID = 4290; + CMD_ID = 4269; } oneof OpInfo { diff --git a/proto/WidgetDoBagRsp.proto b/proto/WidgetDoBagRsp.proto index 898294d1d..e587c62eb 100644 --- a/proto/WidgetDoBagRsp.proto +++ b/proto/WidgetDoBagRsp.proto @@ -9,7 +9,7 @@ message WidgetDoBagRsp { NONE = 0; ENET_CHANNEL_ID = 0; ENET_IS_RELIABLE = 1; - CMD_ID = 4271; + CMD_ID = 4270; } int32 retcode = 1; diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerWidgetDoBagReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerWidgetDoBagReq.java new file mode 100644 index 000000000..d9cee08de --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerWidgetDoBagReq.java @@ -0,0 +1,57 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.GadgetData; +import emu.grasscutter.game.entity.EntityVehicle; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.props.LifeState; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.*; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; +import emu.grasscutter.server.packet.send.PacketWidgetCoolDownNotify; +import emu.grasscutter.server.packet.send.PacketWidgetDoBagRsp; +import emu.grasscutter.server.packet.send.PacketWidgetGadgetDataNotify; +import emu.grasscutter.utils.Position; + +import java.util.List; + +@Opcodes(PacketOpcodes.WidgetDoBagReq) +public class HandlerWidgetDoBagReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + WidgetDoBagReqOuterClass.WidgetDoBagReq req = WidgetDoBagReqOuterClass.WidgetDoBagReq.parseFrom(payload); + switch (req.getMaterialId()) { + case 220026 -> { + GadgetData gadgetData = GameData.getGadgetDataMap().get(70500025); + Position pos = new Position(req.getWidgetCreatorInfo().getLocationInfo().getPos()); + Position rot = new Position(req.getWidgetCreatorInfo().getLocationInfo().getRot()); + GameEntity entity = new EntityVehicle( + session.getPlayer().getScene(), + session.getPlayer(), + gadgetData.getId(), + 0, + pos, + rot + ); + + session.getPlayer().getScene().addEntity(entity); + + session.send(new PacketWidgetGadgetDataNotify(70500025, List.of(entity.getId()))); // ??? + session.send(new PacketWidgetCoolDownNotify(15, System.currentTimeMillis() + 5000L, true)); + session.send(new PacketWidgetCoolDownNotify(15, System.currentTimeMillis() + 5000L, true)); + // Send twice, and I don't know why, Ask mhy + session.send(new PacketWidgetDoBagRsp()); + } + default -> { + session.send(new PacketWidgetDoBagRsp()); + } + + } + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetCoolDownNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetCoolDownNotify.java new file mode 100644 index 000000000..a73187020 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetCoolDownNotify.java @@ -0,0 +1,25 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.WidgetCoolDownDataOuterClass; +import emu.grasscutter.net.proto.WidgetCoolDownNotifyOuterClass; + +public class PacketWidgetCoolDownNotify extends BasePacket { + + public PacketWidgetCoolDownNotify(int id, long coolDownTime, boolean isSuccess) { + super(PacketOpcodes.WidgetCoolDownNotify); + + WidgetCoolDownNotifyOuterClass.WidgetCoolDownNotify proto = WidgetCoolDownNotifyOuterClass.WidgetCoolDownNotify.newBuilder() + .addGroupCoolDownDataList( + WidgetCoolDownDataOuterClass.WidgetCoolDownData.newBuilder() + .setId(id) + .setCoolDownTime(coolDownTime) + .setIsSuccess(isSuccess) + .build() + ) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetDoBagRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetDoBagRsp.java new file mode 100644 index 000000000..7ce5065ea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetDoBagRsp.java @@ -0,0 +1,28 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.WidgetDoBagRspOuterClass; + +public class PacketWidgetDoBagRsp extends BasePacket { + + public PacketWidgetDoBagRsp(int materialId) { + super(PacketOpcodes.WidgetDoBagRsp); + + WidgetDoBagRspOuterClass.WidgetDoBagRsp proto = WidgetDoBagRspOuterClass.WidgetDoBagRsp.newBuilder() + .setMaterialId(materialId) + .setRetcode(0) + .build(); + + this.setData(proto); + } + + public PacketWidgetDoBagRsp() { + super(PacketOpcodes.WidgetDoBagRsp); + + WidgetDoBagRspOuterClass.WidgetDoBagRsp proto = WidgetDoBagRspOuterClass.WidgetDoBagRsp.newBuilder() + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetGadgetDataNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetGadgetDataNotify.java new file mode 100644 index 000000000..f94c6c10e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWidgetGadgetDataNotify.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.WidgetGadgetDataNotifyOuterClass; +import emu.grasscutter.net.proto.WidgetGadgetDataOuterClass; + +import java.io.IOException; +import java.util.List; + +public class PacketWidgetGadgetDataNotify extends BasePacket { + public PacketWidgetGadgetDataNotify(int gadgetId, List gadgetEntityIdList) throws IOException { + super(PacketOpcodes.WidgetGadgetDataNotify); + + WidgetGadgetDataNotifyOuterClass.WidgetGadgetDataNotify proto = WidgetGadgetDataNotifyOuterClass.WidgetGadgetDataNotify.newBuilder() + .setWidgetGadgetData( + WidgetGadgetDataOuterClass.WidgetGadgetData.newBuilder() + .setGadgetId(gadgetId) + .addAllGadgetEntityIdList(gadgetEntityIdList) + .build() + ) + .build(); + + this.setData(proto); + } +} From 295f15eece04e22f12ede08e22ea93d2317837cb Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sat, 14 May 2022 03:20:20 +0800 Subject: [PATCH 326/434] feature(serenitea pot): Implementation of enter Ugly hard code --- proto/HomeChooseModuleReq.proto | 17 +++++ proto/HomeChooseModuleRsp.proto | 17 +++++ proto/HomeComfortInfoNotify.proto | 17 +++++ proto/HomeModuleComfortInfo.proto | 10 +++ proto/PlayerHomeCompInfo.proto | 12 ++++ proto/PlayerHomeCompInfoNotify.proto | 17 +++++ proto/TryEnterHomeReq.proto | 18 +++++ proto/TryEnterHomeRsp.proto | 18 +++++ .../emu/grasscutter/game/player/Player.java | 30 ++++++++ .../recv/HandlerHomeChooseModuleReq.java | 26 +++++++ .../packet/recv/HandlerTryEnterHomeReq.java | 68 +++++++++++++++++++ .../send/PacketHomeChooseModuleRsp.java | 19 ++++++ .../send/PacketHomeComfortInfoNotify.java | 40 +++++++++++ .../send/PacketPlayerHomeCompInfoNotify.java | 32 +++++++++ .../packet/send/PacketTryEnterHomeRsp.java | 30 ++++++++ 15 files changed, 371 insertions(+) create mode 100644 proto/HomeChooseModuleReq.proto create mode 100644 proto/HomeChooseModuleRsp.proto create mode 100644 proto/HomeComfortInfoNotify.proto create mode 100644 proto/HomeModuleComfortInfo.proto create mode 100644 proto/PlayerHomeCompInfo.proto create mode 100644 proto/PlayerHomeCompInfoNotify.proto create mode 100644 proto/TryEnterHomeReq.proto create mode 100644 proto/TryEnterHomeRsp.proto create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChooseModuleReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeChooseModuleRsp.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketHomeComfortInfoNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketTryEnterHomeRsp.java diff --git a/proto/HomeChooseModuleReq.proto b/proto/HomeChooseModuleReq.proto new file mode 100644 index 000000000..9be2b91ab --- /dev/null +++ b/proto/HomeChooseModuleReq.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + + +message HomeChooseModuleReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 4530; + } + + uint32 module_id = 1; +} \ No newline at end of file diff --git a/proto/HomeChooseModuleRsp.proto b/proto/HomeChooseModuleRsp.proto new file mode 100644 index 000000000..7425d419e --- /dev/null +++ b/proto/HomeChooseModuleRsp.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + + +message HomeChooseModuleRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4653; + } + + int32 retcode = 1; + uint32 module_id = 2; +} \ No newline at end of file diff --git a/proto/HomeComfortInfoNotify.proto b/proto/HomeComfortInfoNotify.proto new file mode 100644 index 000000000..e66e14d06 --- /dev/null +++ b/proto/HomeComfortInfoNotify.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "HomeModuleComfortInfo.proto"; + +message HomeComfortInfoNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4557; + } + + repeated HomeModuleComfortInfo module_info_list = 1; +} \ No newline at end of file diff --git a/proto/HomeModuleComfortInfo.proto b/proto/HomeModuleComfortInfo.proto new file mode 100644 index 000000000..d7d54fc31 --- /dev/null +++ b/proto/HomeModuleComfortInfo.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + + +message HomeModuleComfortInfo { + uint32 module_id = 1; + repeated uint32 world_scene_block_comfort_value_list = 2; + uint32 room_scene_comfort_value = 3; +} \ No newline at end of file diff --git a/proto/PlayerHomeCompInfo.proto b/proto/PlayerHomeCompInfo.proto new file mode 100644 index 000000000..a3015df84 --- /dev/null +++ b/proto/PlayerHomeCompInfo.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "FriendEnterHomeOption.proto"; + +message PlayerHomeCompInfo { + FriendEnterHomeOption friend_enter_home_option = 1; + repeated uint32 unlocked_module_id_list = 2; + repeated uint32 levelup_reward_got_level_list = 3; + repeated uint32 seen_module_id_list = 4; +} \ No newline at end of file diff --git a/proto/PlayerHomeCompInfoNotify.proto b/proto/PlayerHomeCompInfoNotify.proto new file mode 100644 index 000000000..61ec3e7f2 --- /dev/null +++ b/proto/PlayerHomeCompInfoNotify.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "PlayerHomeCompInfo.proto"; + +message PlayerHomeCompInfoNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4628; + } + + PlayerHomeCompInfo comp_info = 1; +} \ No newline at end of file diff --git a/proto/TryEnterHomeReq.proto b/proto/TryEnterHomeReq.proto new file mode 100644 index 000000000..220787898 --- /dev/null +++ b/proto/TryEnterHomeReq.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + + +message TryEnterHomeReq { + enum CmdId { + option allow_alias = true; + ENET_CHANNEL_ID = 0; + NONE = 0; + ENET_IS_RELIABLE = 1; + IS_ALLOW_CLIENT = 1; + CMD_ID = 4792; + } + + uint32 target_uid = 1; + uint32 target_point = 2; +} \ No newline at end of file diff --git a/proto/TryEnterHomeRsp.proto b/proto/TryEnterHomeRsp.proto new file mode 100644 index 000000000..a1e1f8c22 --- /dev/null +++ b/proto/TryEnterHomeRsp.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + + +message TryEnterHomeRsp { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4690; + } + + int32 retcode = 1; + uint32 target_uid = 2; + repeated uint32 param_list = 3; +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index b7d8470bf..9c9f1ea56 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -87,6 +87,9 @@ public class Player { private Integer widgetId; + private Set realmList; + private Integer currentRealmId; + @Transient private long nextGuid = 0; @Transient private int peerId; @Transient private World world; @@ -313,6 +316,31 @@ public class Player { this.widgetId = widgetId; } + public Set getRealmList() { + return realmList; + } + + public void setRealmList(Set realmList) { + this.realmList = realmList; + } + + public void addRealmList(int realmId) { + if (this.realmList == null) { + this.realmList = new HashSet<>(); + } else if (this.realmList.contains(realmId)) { + return; + } + this.realmList.add(realmId); + } + + public Integer getCurrentRealmId() { + return currentRealmId; + } + + public void setCurrentRealmId(Integer currentRealmId) { + this.currentRealmId = currentRealmId; + } + public Position getPos() { return pos; } @@ -1187,6 +1215,8 @@ public class Player { session.send(new PacketServerCondMeetQuestListUpdateNotify(this)); session.send(new PacketAllWidgetDataNotify(this)); session.send(new PacketWidgetGadgetAllDataNotify()); + session.send(new PacketPlayerHomeCompInfoNotify(this)); + session.send(new PacketHomeComfortInfoNotify(this)); getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChooseModuleReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChooseModuleReq.java new file mode 100644 index 000000000..5a7c0dbe5 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChooseModuleReq.java @@ -0,0 +1,26 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.HomeChooseModuleReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketHomeChooseModuleRsp; +import emu.grasscutter.server.packet.send.PacketHomeComfortInfoNotify; +import emu.grasscutter.server.packet.send.PacketPlayerHomeCompInfoNotify; + + +@Opcodes(PacketOpcodes.HomeChooseModuleReq) +public class HandlerHomeChooseModuleReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + HomeChooseModuleReqOuterClass.HomeChooseModuleReq req = + HomeChooseModuleReqOuterClass.HomeChooseModuleReq.parseFrom(payload); + session.getPlayer().addRealmList(req.getModuleId()); + session.getPlayer().setCurrentRealmId(req.getModuleId()); + session.send(new PacketHomeChooseModuleRsp(req.getModuleId())); + session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer())); + session.send(new PacketHomeComfortInfoNotify(session.getPlayer())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java new file mode 100644 index 000000000..3e78bcb3a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java @@ -0,0 +1,68 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.TryEnterHomeReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketTryEnterHomeRsp; +import emu.grasscutter.utils.Position; + +@Opcodes(PacketOpcodes.TryEnterHomeReq) +public class HandlerTryEnterHomeReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + TryEnterHomeReqOuterClass.TryEnterHomeReq req = + TryEnterHomeReqOuterClass.TryEnterHomeReq.parseFrom(payload); + + if (req.getTargetUid() != session.getPlayer().getUid()) { + // I hope that tomorrow there will be a hero who can support multiplayer mode and write code like a poem + session.send(new PacketTryEnterHomeRsp()); + return; + } + + // Hardcoded for now + switch (session.getPlayer().getCurrentRealmId()) { + case 1: + session.getPlayer().getWorld().transferPlayerToScene( + session.getPlayer(), + 2001, + new Position(839, 319, 137) + ); + break; + + case 2: + session.getPlayer().getWorld().transferPlayerToScene( + session.getPlayer(), + 2002, + new Position(605, 444, 554) + ); + break; + + case 3: + session.getPlayer().getWorld().transferPlayerToScene( + session.getPlayer(), + 2003, + new Position(511, 229, 605) + ); + break; + + case 4: + session.getPlayer().getWorld().transferPlayerToScene( + session.getPlayer(), + 2004, + new Position(239, 187, 536) + ); + break; + + default: + session.send(new PacketTryEnterHomeRsp()); + return; + } + + + session.send(new PacketTryEnterHomeRsp(req.getTargetUid())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeChooseModuleRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeChooseModuleRsp.java new file mode 100644 index 000000000..e7b3ff1ea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeChooseModuleRsp.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.HomeChooseModuleRspOuterClass; + +public class PacketHomeChooseModuleRsp extends BasePacket { + + public PacketHomeChooseModuleRsp(int moduleId) { + super(PacketOpcodes.HomeChooseModuleRsp); + + HomeChooseModuleRspOuterClass.HomeChooseModuleRsp proto = HomeChooseModuleRspOuterClass.HomeChooseModuleRsp.newBuilder() + .setRetcode(0) + .setModuleId(moduleId) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeComfortInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeComfortInfoNotify.java new file mode 100644 index 000000000..47e46dfdb --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeComfortInfoNotify.java @@ -0,0 +1,40 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.HomeComfortInfoNotifyOuterClass; +import emu.grasscutter.net.proto.HomeModuleComfortInfoOuterClass; + +import java.util.ArrayList; +import java.util.List; + +public class PacketHomeComfortInfoNotify extends BasePacket { + + public PacketHomeComfortInfoNotify(Player player) { + super(PacketOpcodes.HomeComfortInfoNotify); + + if (player.getRealmList() == null) { + // Do not send + return; + } + + List comfortInfoList = new ArrayList<>(); + + for (int moduleId : player.getRealmList()) { + comfortInfoList.add( + HomeModuleComfortInfoOuterClass.HomeModuleComfortInfo.newBuilder() + .setModuleId(moduleId) + .build() + ); + } + + HomeComfortInfoNotifyOuterClass.HomeComfortInfoNotify proto = HomeComfortInfoNotifyOuterClass.HomeComfortInfoNotify + .newBuilder() + .addAllModuleInfoList(comfortInfoList) + .build(); + + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java new file mode 100644 index 000000000..29a6964b5 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java @@ -0,0 +1,32 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PlayerHomeCompInfoNotifyOuterClass; +import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass; + +import java.util.List; + +public class PacketPlayerHomeCompInfoNotify extends BasePacket { + + public PacketPlayerHomeCompInfoNotify(Player player) { + super(PacketOpcodes.PlayerHomeCompInfoNotify); + + if (player.getRealmList() == null) { + // Do not send + return; + } + + PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto = PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder() + .setCompInfo( + PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder() + .addAllUnlockedModuleIdList(player.getRealmList()) + .addAllLevelupRewardGotLevelList(List.of(1)) // Hardcoded + .build() + ) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketTryEnterHomeRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketTryEnterHomeRsp.java new file mode 100644 index 000000000..369c44140 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketTryEnterHomeRsp.java @@ -0,0 +1,30 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.RetcodeOuterClass; +import emu.grasscutter.net.proto.TryEnterHomeRspOuterClass; + +public class PacketTryEnterHomeRsp extends BasePacket { + + public PacketTryEnterHomeRsp() { + super(PacketOpcodes.TryEnterHomeRsp); + + TryEnterHomeRspOuterClass.TryEnterHomeRsp proto = TryEnterHomeRspOuterClass.TryEnterHomeRsp.newBuilder() + .setRetcode(RetcodeOuterClass.Retcode.RET_SVR_ERROR_VALUE) + .build(); + + this.setData(proto); + } + + public PacketTryEnterHomeRsp(int uid) { + super(PacketOpcodes.TryEnterHomeRsp); + + TryEnterHomeRspOuterClass.TryEnterHomeRsp proto = TryEnterHomeRspOuterClass.TryEnterHomeRsp.newBuilder() + .setRetcode(0) + .setTargetUid(uid) + .build(); + + this.setData(proto); + } +} From 4f3112133c590763ec385c2db5aa91dd2fb1be84 Mon Sep 17 00:00:00 2001 From: Yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Sat, 14 May 2022 04:50:31 +0800 Subject: [PATCH 327/434] fix(serenitea pot): teleport & read born pos from lua --- .../emu/grasscutter/game/world/World.java | 5 ++ .../packet/recv/HandlerTryEnterHomeReq.java | 48 +++++-------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java index 95356a15a..22048077d 100644 --- a/src/main/java/emu/grasscutter/game/world/World.java +++ b/src/main/java/emu/grasscutter/game/world/World.java @@ -268,6 +268,11 @@ public class World implements Iterable { } else if (oldScene == newScene) { enterType = EnterType.ENTER_GOTO; } + + // Home + if (2001 <= newScene.getId() && newScene.getId() <= 2004) { + enterType = EnterType.ENTER_SELF_HOME; + } // Teleport packet player.sendPacket(new PacketPlayerEnterSceneNotify(player, enterType, enterReason, sceneId, pos)); diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java index 3e78bcb3a..5df106df2 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java @@ -1,10 +1,12 @@ package emu.grasscutter.server.packet.recv; -import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.world.Scene; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.TryEnterHomeReqOuterClass; +import emu.grasscutter.scripts.data.SceneConfig; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketTryEnterHomeRsp; import emu.grasscutter.utils.Position; @@ -23,44 +25,16 @@ public class HandlerTryEnterHomeReq extends PacketHandler { return; } - // Hardcoded for now - switch (session.getPlayer().getCurrentRealmId()) { - case 1: - session.getPlayer().getWorld().transferPlayerToScene( - session.getPlayer(), - 2001, - new Position(839, 319, 137) - ); - break; + int realmId = 2000 + session.getPlayer().getCurrentRealmId(); - case 2: - session.getPlayer().getWorld().transferPlayerToScene( - session.getPlayer(), - 2002, - new Position(605, 444, 554) - ); - break; + Scene scene = session.getPlayer().getWorld().getSceneById(realmId); + Position pos = scene.getScriptManager().getConfig().born_pos; - case 3: - session.getPlayer().getWorld().transferPlayerToScene( - session.getPlayer(), - 2003, - new Position(511, 229, 605) - ); - break; - - case 4: - session.getPlayer().getWorld().transferPlayerToScene( - session.getPlayer(), - 2004, - new Position(239, 187, 536) - ); - break; - - default: - session.send(new PacketTryEnterHomeRsp()); - return; - } + session.getPlayer().getWorld().transferPlayerToScene( + session.getPlayer(), + realmId, + pos + ); session.send(new PacketTryEnterHomeRsp(req.getTargetUid())); From 44456e2868376aa267dfb428af4c1afd29642bd6 Mon Sep 17 00:00:00 2001 From: ShigemoriHakura <62388797+ShigemoriHakura@users.noreply.github.com> Date: Sat, 14 May 2022 07:33:07 +0800 Subject: [PATCH 328/434] Add support for codexQuests (#870) --- proto/CodexDataFullNotify.proto | 16 ++++++ proto/CodexDataUpdateNotify.proto | 18 +++++++ proto/CodexType.proto | 15 ++++++ proto/CodexTypeComparer.proto | 6 +++ proto/CodexTypeData.proto | 11 ++++ .../java/emu/grasscutter/data/GameData.java | 7 ++- .../emu/grasscutter/data/def/CodexQuest.java | 42 +++++++++++++++ .../emu/grasscutter/game/player/Player.java | 1 + .../grasscutter/game/quest/GameMainQuest.java | 2 + .../emu/grasscutter/game/quest/GameQuest.java | 1 + .../grasscutter/game/quest/QuestManager.java | 6 +++ .../send/PacketCodexDataFullNotify.java | 54 +++++++++++++++++++ .../send/PacketCodexDataUpdateNotify.java | 27 ++++++++++ 13 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 proto/CodexDataFullNotify.proto create mode 100644 proto/CodexDataUpdateNotify.proto create mode 100644 proto/CodexType.proto create mode 100644 proto/CodexTypeComparer.proto create mode 100644 proto/CodexTypeData.proto create mode 100644 src/main/java/emu/grasscutter/data/def/CodexQuest.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataFullNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataUpdateNotify.java diff --git a/proto/CodexDataFullNotify.proto b/proto/CodexDataFullNotify.proto new file mode 100644 index 000000000..27e26e4de --- /dev/null +++ b/proto/CodexDataFullNotify.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "CodexTypeData.proto"; + +message CodexDataFullNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4208; + } + + repeated CodexTypeData type_data_list = 1; +} diff --git a/proto/CodexDataUpdateNotify.proto b/proto/CodexDataUpdateNotify.proto new file mode 100644 index 000000000..f309ca183 --- /dev/null +++ b/proto/CodexDataUpdateNotify.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "CodexType.proto"; + +message CodexDataUpdateNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4205; + } + + CodexType type = 1; + uint32 id = 2; + uint32 weapon_max_promote_level = 3; +} diff --git a/proto/CodexType.proto b/proto/CodexType.proto new file mode 100644 index 000000000..d545966f3 --- /dev/null +++ b/proto/CodexType.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +enum CodexType { + CODEX_NONE = 0; + CODEX_QUEST = 1; + CODEX_WEAPON = 2; + CODEX_ANIMAL = 3; + CODEX_MATERIAL = 4; + CODEX_BOOKS = 5; + CODEX_PUSHTIPS = 6; + CODEX_VIEW = 7; + CODEX_RELIQUARY = 8; +} diff --git a/proto/CodexTypeComparer.proto b/proto/CodexTypeComparer.proto new file mode 100644 index 000000000..d87068c90 --- /dev/null +++ b/proto/CodexTypeComparer.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CodexTypeComparer { +} diff --git a/proto/CodexTypeData.proto b/proto/CodexTypeData.proto new file mode 100644 index 000000000..95e247379 --- /dev/null +++ b/proto/CodexTypeData.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; +import "CodexType.proto"; + +message CodexTypeData { + CodexType type = 1; + repeated uint32 codex_id_list = 2; + repeated bool have_viewed_list = 3; + map weapon_max_promote_level_map = 4; +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 75b840202..ed5c469bb 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -9,7 +9,6 @@ import java.util.Map; import emu.grasscutter.Grasscutter; import emu.grasscutter.utils.Utils; import emu.grasscutter.data.custom.AbilityEmbryoEntry; -import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.OpenConfigEntry; import emu.grasscutter.data.custom.MainQuestData; @@ -65,6 +64,8 @@ public class GameData { private static final Int2ObjectMap sceneDataMap = new Int2ObjectLinkedOpenHashMap<>(); private static final Int2ObjectMap fetterDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap codexQuestMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap codexQuestIdMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap fetterCharacterCardDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap rewardDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap worldLevelDataMap = new Int2ObjectOpenHashMap<>(); @@ -293,6 +294,10 @@ public class GameData { return fetters; } + public static Int2ObjectMap getCodexQuestMap(){return codexQuestMap;} + + public static Int2ObjectMap getCodexQuestIdMap(){return codexQuestIdMap;} + public static Int2ObjectMap getWorldLevelDataMap() { return worldLevelDataMap; } diff --git a/src/main/java/emu/grasscutter/data/def/CodexQuest.java b/src/main/java/emu/grasscutter/data/def/CodexQuest.java new file mode 100644 index 000000000..578837e04 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/def/CodexQuest.java @@ -0,0 +1,42 @@ +package emu.grasscutter.data.def; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; + +@ResourceType(name = {"QuestCodexExcelConfigData.json"}, loadPriority = ResourceType.LoadPriority.HIGH) +public class CodexQuest extends GameResource { + private int Id; + private int ParentQuestId; + private int ChapterId; + private int SortOrder; + private boolean IsDisuse; + + public int getParentQuestId() { + return ParentQuestId; + } + + public int getId() { + return Id; + } + + public int getChapterId() { + return ChapterId; + } + + public int getSortOrder() { + return SortOrder; + } + + public boolean getIsDisuse() { + return IsDisuse; + } + + @Override + public void onLoad() { + if(!this.getIsDisuse()) { + GameData.getCodexQuestIdMap().put(this.getParentQuestId(), this); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 9c9f1ea56..f86e09370 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -1212,6 +1212,7 @@ public class Player { session.send(new PacketAvatarDataNotify(this)); session.send(new PacketFinishedParentQuestNotify(this)); session.send(new PacketQuestListNotify(this)); + session.send(new PacketCodexDataFullNotify(this)); session.send(new PacketServerCondMeetQuestListUpdateNotify(this)); session.send(new PacketAllWidgetDataNotify(this)); session.send(new PacketWidgetGadgetAllDataNotify()); diff --git a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java index 613819d0b..c298913cc 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java @@ -3,6 +3,7 @@ package emu.grasscutter.game.quest; import java.util.HashMap; import java.util.Map; +import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify; import org.bson.types.ObjectId; import dev.morphia.annotations.Entity; @@ -91,6 +92,7 @@ public class GameMainQuest { this.isFinished = true; this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED; this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this)); + this.getOwner().getSession().send(new PacketCodexDataUpdateNotify(this)); this.save(); } diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java index 5e1126fcb..3caf950ba 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -11,6 +11,7 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.game.quest.enums.LogicType; import emu.grasscutter.game.quest.enums.QuestState; import emu.grasscutter.net.proto.QuestOuterClass.Quest; +import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify; import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify; import emu.grasscutter.utils.Utils; diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index 745ce9ef8..0d81834f0 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -66,6 +66,12 @@ public class QuestManager { } } } + + public void forEachMainQuest(Consumer callback) { + for (GameMainQuest mainQuest : getQuests().values()) { + callback.accept(mainQuest); + } + } // TODO public void forEachActiveQuest(Consumer callback) { diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataFullNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataFullNotify.java new file mode 100644 index 000000000..760c3b3d2 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataFullNotify.java @@ -0,0 +1,54 @@ +package emu.grasscutter.server.packet.send; + +import java.util.Collections; +import java.util.List; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CodexDataFullNotifyOuterClass.CodexDataFullNotify; +import emu.grasscutter.net.proto.CodexTypeDataOuterClass.CodexTypeData; +import emu.grasscutter.net.proto.CodexTypeOuterClass; +import emu.grasscutter.server.game.GameSession; + +public class PacketCodexDataFullNotify extends BasePacket { + public PacketCodexDataFullNotify(Player player) { + super(PacketOpcodes.CodexDataFullNotify, true); + + //Quests + CodexTypeData.Builder questTypeData = CodexTypeData.newBuilder() + .setTypeValue(1); + + //Tips + CodexTypeData.Builder pushTipsTypeData = CodexTypeData.newBuilder() + .setTypeValue(6); + + //Views + CodexTypeData.Builder viewTypeData = CodexTypeData.newBuilder() + .setTypeValue(7); + + //Weapons + CodexTypeData.Builder weaponTypeData = CodexTypeData.newBuilder() + .setTypeValue(2); + + + player.getQuestManager().forEachMainQuest(mainQuest -> { + if(mainQuest.isFinished()){ + var codexQuest = GameData.getCodexQuestIdMap().get(mainQuest.getParentQuestId()); + if(codexQuest != null){ + questTypeData.addCodexIdList(codexQuest.getId()).addAllHaveViewedList(Collections.singleton(true)); + } + } + }); + + CodexDataFullNotify.Builder proto = CodexDataFullNotify.newBuilder() + .addTypeDataList(questTypeData.build()) + .addTypeDataList(pushTipsTypeData.build()) + .addTypeDataList(viewTypeData.build()) + .addTypeDataList(weaponTypeData); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataUpdateNotify.java new file mode 100644 index 000000000..c7318bd91 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCodexDataUpdateNotify.java @@ -0,0 +1,27 @@ +package emu.grasscutter.server.packet.send; + +import java.util.Collections; +import java.util.List; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CodexDataUpdateNotifyOuterClass.CodexDataUpdateNotify; +import emu.grasscutter.server.game.GameSession; + +public class PacketCodexDataUpdateNotify extends BasePacket { + public PacketCodexDataUpdateNotify(GameMainQuest quest) { + super(PacketOpcodes.CodexDataUpdateNotify, true); + var codexQuest = GameData.getCodexQuestIdMap().get(quest.getParentQuestId()); + if(codexQuest != null){ + CodexDataUpdateNotify proto = CodexDataUpdateNotify.newBuilder() + .setTypeValue(1) + .setId(codexQuest.getId()) + .build(); + this.setData(proto); + } + } +} From b78e39705660b2243e95275cccabc524c4bc18d8 Mon Sep 17 00:00:00 2001 From: tester233 <105267106+tester233@users.noreply.github.com> Date: Fri, 13 May 2022 23:36:30 +0800 Subject: [PATCH 329/434] Improve text --- src/main/resources/languages/zh-CN.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 4ece73fd9..aaeac5a86 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -17,7 +17,7 @@ "default_password": "[Dispatch] 成功加载 keystore 默认密码。请考虑将 config.json 的默认密码设置为 123456" }, "authentication": { - "default_unable_to_verify": "[Authentication] 称为 verifyUser 的 method 在默认验证程序中不可用" + "default_unable_to_verify": "[Authentication] 称为 verifyUser 的方法在默认验证程序中不可用" }, "no_commands_error": "此命令不适用于 Dispatch-only 模式", "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", @@ -215,6 +215,14 @@ "success": "坐标:%s, %s, %s\n场景ID:%s", "description": "获取所在位置" }, + "quest": { + "description": "添加或完成任务", + "usage": "quest [任务ID]", + "added": "已添加任务 %s", + "finished": "已完成任务 %s", + "not_found": "未找到任务", + "invalid_id": "无效的任务ID" + }, "reload": { "reload_start": "正在重载配置文件和数据。", "reload_done": "重载完成。", @@ -356,9 +364,9 @@ "gacha": { "details": { "title": "祈愿详情", - "available_five_stars": "出现的五星物品", - "available_four_stars": "出现的四星物品", - "available_three_stars": "出现的三星物品", + "available_five_stars": "可获得的5星物品", + "available_four_stars": "可获得的4星物品", + "available_three_stars": "可获得的3星物品", "template_missing": "缺失文件:data/gacha_details.html" } } From ab5e4fcdb412fd4a0a97245aa041edb1291f2e41 Mon Sep 17 00:00:00 2001 From: Melledy <52122272+Melledy@users.noreply.github.com> Date: Fri, 13 May 2022 16:35:03 -0700 Subject: [PATCH 330/434] Use scene types instead of hardcoding scene ids for checking enter reason --- src/main/java/emu/grasscutter/game/world/World.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java index 22048077d..ccbe4b841 100644 --- a/src/main/java/emu/grasscutter/game/world/World.java +++ b/src/main/java/emu/grasscutter/game/world/World.java @@ -10,6 +10,7 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player.SceneLoadState; import emu.grasscutter.game.props.EnterReason; import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.SceneType; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.DungeonData; import emu.grasscutter.data.def.SceneData; @@ -267,11 +268,9 @@ public class World implements Iterable { enterReason = EnterReason.DungeonEnter; } else if (oldScene == newScene) { enterType = EnterType.ENTER_GOTO; - } - - // Home - if (2001 <= newScene.getId() && newScene.getId() <= 2004) { - enterType = EnterType.ENTER_SELF_HOME; + } else if (newScene.getSceneType() == SceneType.SCENE_HOME_WORLD) { + // Home + enterType = EnterType.ENTER_SELF_HOME; } // Teleport packet From 57a9cae1a47973cd1a0c776499a516ac412e4ccf Mon Sep 17 00:00:00 2001 From: ShiroSaki <62388797+ShigemoriHakura@users.noreply.github.com> Date: Sat, 14 May 2022 04:31:27 +0800 Subject: [PATCH 331/434] add support for announcement page --- .gitignore | 1 + data/GameAnnouncementList.json | 10 ++- .../server/dispatch/DispatchServer.java | 6 ++ .../http/AnnouncementIndexHandler.java | 61 +++++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/main/java/emu/grasscutter/server/dispatch/http/AnnouncementIndexHandler.java diff --git a/.gitignore b/.gitignore index 6fd78ed3b..9a298a89a 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ BuildConfig.java # macOS .DS_Store +data/hk4e/announcement/ diff --git a/data/GameAnnouncementList.json b/data/GameAnnouncementList.json index f6566960a..7464b3b0f 100644 --- a/data/GameAnnouncementList.json +++ b/data/GameAnnouncementList.json @@ -57,5 +57,13 @@ "mi18n_name": "Activity" } ], - "timezone": -5 + "timezone": -5, + "alert": false, + "alert_id": 0, + "pic_list": [], + "pic_total": 0, + "pic_type_list": [], + "pic_alert": false, + "pic_alert_id": 0, + "static_sign": "" } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 7e439a1a4..7153542a4 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -16,6 +16,7 @@ import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler; import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; +import emu.grasscutter.server.dispatch.http.AnnouncementIndexHandler; import emu.grasscutter.server.dispatch.http.GachaDetailsHandler; import emu.grasscutter.server.dispatch.http.GachaRecordHandler; import emu.grasscutter.server.dispatch.json.*; @@ -443,6 +444,11 @@ public final class DispatchServer { // gacha details httpServer.get("/gacha/details", new GachaDetailsHandler()); + // announcement index + httpServer.get("/hk4e/announcement/*", new AnnouncementIndexHandler()); + httpServer.get("/sw.js", new AnnouncementIndexHandler()); + httpServer.get("/dora/lib/vue/2.6.11/vue.min.js", new AnnouncementIndexHandler()); + // static file support for plugins httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/AnnouncementIndexHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/AnnouncementIndexHandler.java new file mode 100644 index 000000000..7e55eac7e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/dispatch/http/AnnouncementIndexHandler.java @@ -0,0 +1,61 @@ +package emu.grasscutter.server.dispatch.http; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.http.HttpContextHandler; +import express.http.Request; +import express.http.Response; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static emu.grasscutter.Configuration.DATA; + +public class AnnouncementIndexHandler implements HttpContextHandler { + private final String render_template; + private final String render_swjs; + private final String render_vueminjs; + + public AnnouncementIndexHandler() { + File template = new File(Utils.toFilePath(DATA("/hk4e/announcement/index.html"))); + File swjs = new File(Utils.toFilePath(DATA("/hk4e/announcement/sw.js"))); + File vueminjs = new File(Utils.toFilePath(DATA("/hk4e/announcement/vue.min.js"))); + this.render_template = template.exists() ? new String(FileUtils.read(template)) : null; + this.render_swjs = swjs.exists() ? new String(FileUtils.read(swjs)) : null; + this.render_vueminjs = vueminjs.exists() ? new String(FileUtils.read(vueminjs)) : null; + } + + @Override + public void handle(Request req, Response res) throws IOException { + if (Objects.equals(req.path(), "/sw.js")) { + res.send(render_swjs); + }else if(Objects.equals(req.path(), "/hk4e/announcement/index.html")) { + res.send(render_template); + }else if(Objects.equals(req.path(), "/dora/lib/vue/2.6.11/vue.min.js")){ + res.send(render_vueminjs); + }else{ + File renderFile = new File(Utils.toFilePath(DATA(req.path()))); + if(renderFile.exists()){ + String ext = req.path().substring(req.path().lastIndexOf(".") + 1); + switch(ext){ + case "css": + res.type("text/css"); + res.send(FileUtils.read(renderFile)); + break; + case "js": + default: + res.send(FileUtils.read(renderFile)); + break; + } + }else{ + Grasscutter.getLogger().info( "File not exist: " + req.path()); + } + } + + + } +} From 87bfc25ab822c6853db689f6232b6c4612ead1db Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 23:22:30 -0400 Subject: [PATCH 332/434] Refactor dispatch (now called HTTP) server (pt. 2) --- .../java/emu/grasscutter/Grasscutter.java | 18 +- .../command/commands/ReloadCommand.java | 2 +- .../grasscutter/game/gacha/GachaBanner.java | 6 +- .../grasscutter/plugin/api/ServerHook.java | 43 +- .../scripts/serializer/LuaSerializer.java | 6 +- .../server/dispatch/AnnouncementHandler.java | 38 -- .../server/dispatch/ClientLogHandler.java | 19 - .../server/dispatch/DispatchServer.java | 532 ------------------ .../authentication/AuthenticationHandler.java | 16 - .../DefaultAuthenticationHandler.java | 80 --- .../dispatch/http/GachaRecordHandler.java | 54 -- .../dispatch/json/ComboTokenReqJson.java | 15 - .../dispatch/json/ComboTokenResJson.java | 17 - .../json/LoginAccountRequestJson.java | 7 - .../server/dispatch/json/LoginResultJson.java | 38 -- .../dispatch/json/LoginTokenRequestJson.java | 6 - .../grasscutter/server/http/HttpServer.java | 4 +- .../server/http/dispatch/RegionHandler.java | 14 +- .../http/handlers/AnnouncementsHandler.java | 8 +- .../server/http/handlers/GachaHandler.java | 71 +++ .../server/http/handlers/GenericHandler.java | 44 +- .../http/handlers/LegacyAuthHandler.java | 17 + .../server/http/handlers/LogHandler.java | 12 +- .../objects/HttpJsonResponse.java} | 6 +- .../packet/send/PacketPlayerLoginRsp.java | 4 +- 25 files changed, 195 insertions(+), 882 deletions(-) delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java rename src/main/java/emu/grasscutter/server/{dispatch/DispatchHttpJsonHandler.java => http/objects/HttpJsonResponse.java} (90%) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index bddfa9964..30768bcb5 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -11,9 +11,7 @@ import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.server.http.HttpServer; import emu.grasscutter.server.http.dispatch.DispatchHandler; -import emu.grasscutter.server.http.handlers.AnnouncementsHandler; -import emu.grasscutter.server.http.handlers.GenericHandler; -import emu.grasscutter.server.http.handlers.LogHandler; +import emu.grasscutter.server.http.handlers.*; import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; @@ -33,7 +31,6 @@ import ch.qos.logback.classic.Logger; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.database.DatabaseManager; import emu.grasscutter.utils.Language; -import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; @@ -54,7 +51,6 @@ public final class Grasscutter { private static int day; // Current day of week. - private static DispatchServer dispatchServer; private static HttpServer httpServer; private static GameServer gameServer; private static PluginManager pluginManager; @@ -113,11 +109,10 @@ public final class Grasscutter { authenticationSystem = new DefaultAuthentication(); // Create server instances. - dispatchServer = new DispatchServer(); httpServer = new HttpServer(); gameServer = new GameServer(); // Create a server hook instance with both servers. - new ServerHook(gameServer, dispatchServer); + new ServerHook(gameServer, httpServer); // Create plugin manager instance. pluginManager = new PluginManager(); @@ -129,14 +124,15 @@ public final class Grasscutter { httpServer.addRouter(GenericHandler.class); httpServer.addRouter(AnnouncementsHandler.class); httpServer.addRouter(DispatchHandler.class); + httpServer.addRouter(LegacyAuthHandler.class); + httpServer.addRouter(GachaHandler.class); // Start servers. var runMode = SERVER.runMode; if (runMode == ServerRunMode.HYBRID) { - dispatchServer.start(); + httpServer.start(); gameServer.start(); } else if (runMode == ServerRunMode.DISPATCH_ONLY) { - dispatchServer.start(); httpServer.start(); } else if (runMode == ServerRunMode.GAME_ONLY) { gameServer.start(); @@ -258,8 +254,8 @@ public final class Grasscutter { return gson; } - public static DispatchServer getDispatchServer() { - return dispatchServer; + public static HttpServer getHttpServer() { + return httpServer; } public static GameServer getGameServer() { diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 9414a89c4..984fd7d60 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -21,7 +21,7 @@ public final class ReloadCommand implements CommandHandler { Grasscutter.getGameServer().getGachaManager().load(); Grasscutter.getGameServer().getDropManager().load(); Grasscutter.getGameServer().getShopManager().load(); - Grasscutter.getDispatchServer().loadQueries(); + // Grasscutter.getHttpServer().loadQueries(); // Is this practical? CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index dce433fcf..7602f7c06 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -98,9 +98,9 @@ public class GachaBanner { } public GachaInfo toProto(String sessionKey) { - String record = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + String record = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); GachaInfo.Builder info = GachaInfo.newBuilder() diff --git a/src/main/java/emu/grasscutter/plugin/api/ServerHook.java b/src/main/java/emu/grasscutter/plugin/api/ServerHook.java index a37abfb62..ffa19110d 100644 --- a/src/main/java/emu/grasscutter/plugin/api/ServerHook.java +++ b/src/main/java/emu/grasscutter/plugin/api/ServerHook.java @@ -1,10 +1,13 @@ package emu.grasscutter.plugin.api; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; -import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.http.HttpServer; +import emu.grasscutter.server.http.Router; import java.util.LinkedList; import java.util.List; @@ -15,7 +18,7 @@ import java.util.List; public final class ServerHook { private static ServerHook instance; private final GameServer gameServer; - private final DispatchServer dispatchServer; + private final HttpServer httpServer; /** * Gets the server hook instance. @@ -28,11 +31,11 @@ public final class ServerHook { /** * Hooks into a server. * @param gameServer The game server to hook into. - * @param dispatchServer The dispatch server to hook into. + * @param httpServer The HTTP server to hook into. */ - public ServerHook(GameServer gameServer, DispatchServer dispatchServer) { + public ServerHook(GameServer gameServer, HttpServer httpServer) { this.gameServer = gameServer; - this.dispatchServer = dispatchServer; + this.httpServer = httpServer; instance = this; } @@ -45,10 +48,10 @@ public final class ServerHook { } /** - * @return The dispatch server. + * @return The HTTP server. */ - public DispatchServer getDispatchServer() { - return this.dispatchServer; + public HttpServer getHttpServer() { + return this.httpServer; } /** @@ -70,4 +73,28 @@ public final class ServerHook { Command commandData = clazz.getAnnotation(Command.class); this.gameServer.getCommandMap().registerCommand(commandData.label(), handler); } + + /** + * Adds a router using an instance of a class. + * @param router A router instance. + */ + public void addRouter(Router router) { + this.addRouter(router.getClass()); + } + + /** + * Adds a router using a class. + * @param router The class of the router. + */ + public void addRouter(Class router) { + this.httpServer.addRouter(router); + } + + /** + * Sets the server's authentication system. + * @param authSystem An instance of the authentication system. + */ + public void setAuthSystem(AuthenticationSystem authSystem) { + Grasscutter.setAuthenticationSystem(authSystem); + } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java index a63328b55..8924a33ae 100644 --- a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java +++ b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java @@ -70,16 +70,14 @@ public class LuaSerializer implements Serializer { } try { + //noinspection ConfusingArgumentToVarargsMethod object = type.getDeclaredConstructor().newInstance(null); LuaValue[] keys = table.keys(); for (LuaValue k : keys) { try { Field field = object.getClass().getDeclaredField(k.checkjstring()); - if (field == null) { - continue; - } - + field.setAccessible(true); LuaValue keyValue = table.get(k); diff --git a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java deleted file mode 100644 index ab752c8e1..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import emu.grasscutter.Grasscutter; -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Objects; - -import static emu.grasscutter.Configuration.*; - -public final class AnnouncementHandler implements HttpContextHandler { - @Override - public void handle(Request request, Response response) throws IOException {//event - if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}"); - } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { - String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); - } - } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static String readToString(File file) { - long length = file.length(); - byte[] content = new byte[(int) length]; - try { - FileInputStream in = new FileInputStream(file); - in.read(content); in.close(); - } catch (IOException ignored) { - Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); - } - - return new String(content); - } -} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java b/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java deleted file mode 100644 index b3d48dbbb..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java +++ /dev/null @@ -1,19 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import java.io.IOException; - -/** - * Used for processing crash dumps and logs generated by the game. - * Logs are in JSON, and are sent to the server for logging. - */ -public final class ClientLogHandler implements HttpContextHandler { - @Override - public void handle(Request request, Response response) throws IOException { - // TODO: Figure out how to dump request body and log to file. - response.send("{\"code\":0}"); - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java deleted file mode 100644 index 8b8a9a185..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ /dev/null @@ -1,532 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.protobuf.ByteString; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.Grasscutter.ServerDebugMode; -import emu.grasscutter.Grasscutter.ServerRunMode; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; -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.authentication.AuthenticationHandler; -import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; -import emu.grasscutter.server.dispatch.http.GachaRecordHandler; -import emu.grasscutter.server.dispatch.json.*; -import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; -import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; -import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; -import emu.grasscutter.tools.Tools; -import emu.grasscutter.utils.FileUtils; -import emu.grasscutter.utils.Utils; -import express.Express; -import io.javalin.http.staticfiles.Location; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import java.io.*; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static emu.grasscutter.utils.Language.translate; -import static emu.grasscutter.Configuration.*; - -public final class DispatchServer { - public static String query_region_list = ""; - public static String query_cur_region = ""; - - private final Gson gson; - private final String defaultServerName = "os_usa"; - - public String regionListBase64; - public Map regions; - private AuthenticationHandler authHandler; - private Express httpServer; - - public DispatchServer() { - this.regions = new HashMap<>(); - this.gson = new GsonBuilder().create(); - - this.loadQueries(); - this.initRegion(); - } - - public Express getServer() { - return httpServer; - } - - public void setHttpServer(Express httpServer) { - this.httpServer.stop(); - this.httpServer = httpServer; - this.httpServer.listen(DISPATCH_INFO.bindPort); - } - - public Gson getGsonFactory() { - return gson; - } - - public QueryCurrRegionHttpRsp getCurrRegion() { - // Needs to be fixed by having the game servers connect to the dispatch server. - if (SERVER.runMode == ServerRunMode.HYBRID) { - return regions.get(defaultServerName).parsedRegionQuery; - } - - Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()"); - return null; - } - - public void loadQueries() { - File file; - - file = new File(DATA("query_region_list.txt")); - if (file.exists()) { - query_region_list = new String(FileUtils.read(file)); - } else { - Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list."); - } - - file = new File(DATA("query_cur_region.txt")); - if (file.exists()) { - query_cur_region = new String(FileUtils.read(file)); - } else { - Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region."); - } - } - - private void initRegion() { - try { - byte[] decoded = Base64.getDecoder().decode(query_region_list); - QueryRegionListHttpRsp rl = QueryRegionListHttpRsp.parseFrom(decoded); - - byte[] decoded2 = Base64.getDecoder().decode(query_cur_region); - QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2); - - List servers = new ArrayList<>(); - List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. - if (SERVER.runMode == ServerRunMode.HYBRID) { // Automatically add the game server if in hybrid mode. - RegionSimpleInfo server = RegionSimpleInfo.newBuilder() - .setName("os_usa") - .setTitle(DISPATCH_INFO.defaultName) - .setType("DEV_PUBLIC") - .setDispatchUrl( - "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) - + "/query_cur_region/" + defaultServerName) - .build(); - usedNames.add(defaultServerName); - servers.add(server); - - RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp(lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress)) - .setGateserverPort(lr(GAME_INFO.accessPort, GAME_INFO.bindPort)) - .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_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 (DISPATCH_INFO.regions.length == 0) { - Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); - System.exit(1); - } - - for (var regionInfo : DISPATCH_INFO.regions) { - 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( - "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) - + "/query_cur_region/" + regionInfo.Name) - .build(); - usedNames.add(regionInfo.Name); - servers.add(server); - - RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp(regionInfo.Ip) - .setGateserverPort(regionInfo.Port) - .setSecretKey(ByteString - .copyFrom(FileUtils.read(KEYS_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() - .addAllRegionList(servers) - .setClientSecretKey(rl.getClientSecretKey()) - .setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted()) - .setEnableLoginPc(true) - .build(); - - this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray()); - } catch (Exception exception) { - Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", exception); - } - } - - public void start() throws Exception { - httpServer = new Express(config -> { - config.server(() -> { - Server server = new Server(); - ServerConnector serverConnector; - - if(HTTP_ENCRYPTION.useEncryption) { - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - File keystoreFile = new File(HTTP_ENCRYPTION.keystore); - - if(keystoreFile.exists()) { - try { - sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); - } catch (Exception e) { - e.printStackTrace(); - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); - - try { - sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword("123456"); - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password")); - } catch (Exception e2) { - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error")); - e2.printStackTrace(); - } - } - - serverConnector = new ServerConnector(server, sslContextFactory); - } else { - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); - HTTP_ENCRYPTION.useEncryption = false; - - serverConnector = new ServerConnector(server); - } - } else { - serverConnector = new ServerConnector(server); - } - - serverConnector.setPort(DISPATCH_INFO.bindPort); - server.setConnectors(new Connector[]{serverConnector}); - return server; - }); - - config.enforceSsl = HTTP_ENCRYPTION.useEncryption; - if(SERVER.debugLevel == ServerDebugMode.ALL) { - config.enableDevLogging(); - } - - if (HTTP_POLICIES.cors.enabled) { - var corsPolicy = HTTP_POLICIES.cors; - if (corsPolicy.allowedOrigins.length > 0) - config.enableCorsForOrigin(corsPolicy.allowedOrigins); - else config.enableCorsForAllOrigins(); - } - }); - - httpServer.get("/", (req, res) -> res.send("" + translate("messages.status.welcome") + "")); - - httpServer.raw().error(404, ctx -> { - if(SERVER.debugLevel == ServerDebugMode.MISSING) { - Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url())); - } - ctx.contentType("text/html"); - ctx.result(""); // I'm like 70% sure this won't break anything. - }); - - // Authentication Handler - // These routes are so that authentication routes are always the same no matter what auth system is used. - httpServer.get("/authentication/type", (req, res) -> { - res.send(this.getAuthHandler().getClass().getName()); - }); - - httpServer.post("/authentication/login", (req, res) -> this.getAuthHandler().handleLogin(req, res)); - httpServer.post("/authentication/register", (req, res) -> this.getAuthHandler().handleRegister(req, res)); - httpServer.post("/authentication/change_password", (req, res) -> this.getAuthHandler().handleChangePassword(req, res)); - - // Server Status - httpServer.get("/status/server", (req, res) -> { - - int playerCount = Grasscutter.getGameServer().getPlayers().size(); - String version = GameConstants.VERSION; - - res.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}"); - }); - - // Dispatch - httpServer.get("/query_region_list", (req, res) -> { - // Log - Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", req.ip())); - - // Invoke event. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListBase64); event.call(); - // Respond with event result. - res.send(event.getRegionList()); - }); - - // /server/:id -> 2.6.5x - httpServer.get("/query_cur_region/:id", (req, res) -> { - String regionName = req.params("id"); - // Log - Grasscutter.getLogger().info( - String.format("Client %s request: query_cur_region/%s", req.ip(), regionName)); - // Create a response form the request query parameters - String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (req.query().values().size() > 0) { - response = regions.get(regionName).Base64; - } - - // Invoke event. - QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call(); - // Respond with event result. - res.send(event.getRegionInfo()); - }); - - // Login - - httpServer.post("/hk4e_global/mdk/shield/api/login", (req, res) -> { - // Get post data - LoginAccountRequestJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class); - } catch (Exception ignored) { } - - // Create response json - if (requestData == null) { - return; - } - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", req.ip())); - - res.send(this.getAuthHandler().handleGameLogin(req, requestData)); - }); - - // Login via token - httpServer.post("/hk4e_global/mdk/shield/api/verify", (req, res) -> { - // Get post data - LoginTokenRequestJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class); - } catch (Exception ignored) { - } - - // Create response json - if (requestData == null) { - return; - } - LoginResultJson responseData = new LoginResultJson(); - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", req.ip())); - - // Login - Account account = DatabaseHelper.getAccountById(requestData.uid); - - // Test - if (account == null || !account.getSessionKey().equals(requestData.token)) { - responseData.retcode = -111; - responseData.message = translate("messages.dispatch.account.account_cache_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", req.ip())); - } 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(translate("messages.dispatch.account.login_token_success", req.ip(), requestData.uid)); - } - - res.send(responseData); - }); - - // Exchange for combo token - httpServer.post("/hk4e_global/combo/granter/login/v2/login", (req, res) -> { - // Get post data - ComboTokenReqJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class); - } catch (Exception ignored) { - } - - // Create response json - if (requestData == null || requestData.data == null) { - return; - } - LoginTokenData loginData = getGsonFactory().fromJson(requestData.data, LoginTokenData.class); // Get login - // data - ComboTokenResJson responseData = new ComboTokenResJson(); - - // Login - Account account = DatabaseHelper.getAccountById(loginData.uid); - - // Test - if (account == null || !account.getSessionKey().equals(loginData.token)) { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.session_key_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", req.ip())); - } else { - responseData.message = "OK"; - responseData.data.open_id = loginData.uid; - responseData.data.combo_id = "157795300"; - responseData.data.combo_token = account.generateLoginToken(); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", req.ip())); - } - - res.send(responseData); - }); - - // TODO: There are some missing route request types here (You can tell if they are missing if they are .all and not anything else) - // When http requests for theses routes are found please remove it from the list in DispatchHttpJsonHandler and update the route request types here - - // Agreement and Protocol - // hk4e-sdk-os.hoyoverse.com - httpServer.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); - // hk4e-sdk-os.hoyoverse.com - // this could be either GET or POST based on the observation of different clients - httpServer.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); - - // Game data - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new AnnouncementHandler()); - // hk4e-api-os-static.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new AnnouncementHandler()); - // hk4e-sdk-os.hoyoverse.com - httpServer.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); - - // Captcha - // api-account-os.hoyoverse.com - httpServer.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); - - // Config - // sdk-os-static.hoyoverse.com - httpServer.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); - // hk4e-sdk-os-static.hoyoverse.com - httpServer.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}")); - // hk4e-sdk-os-static.hoyoverse.com - httpServer.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}")); - // Test api? - // abtest-api-data-sg.hoyoverse.com - httpServer.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); - - // log-upload-os.mihoyo.com - httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); - // /perf/config/verify?device_id=xxx&platform=x&name=xxx - httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); - - // Logging servers - // overseauspider.yuanshen.com - httpServer.all("/log", new ClientLogHandler()); - // log-upload-os.mihoyo.com - httpServer.all("/crash/dataUpload", new ClientLogHandler()); - - // webstatic-sea.hoyoverse.com - httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); - - // gacha record. - String gachaMappingsPath = Utils.toFilePath(DATA("/gacha_mappings.js")); - // TODO: Only serve the html page and have a subsequent request to fetch the gacha data. - httpServer.get("/gacha", new GachaRecordHandler()); - if(!(new File(gachaMappingsPath).exists())) { - Tools.createGachaMapping(gachaMappingsPath); - } - - httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL); - - // static file support for plugins - httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files - - httpServer.listen(DISPATCH_INFO.bindPort); - Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(httpServer.raw().port()))); - } - - private Map parseQueryString(String qs) { - 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; - } - - if (next > last) { - int eqPos = qs.indexOf('=', last); - if (eqPos < 0 || eqPos > next) { - result.put(URLDecoder.decode(qs.substring(last, next), StandardCharsets.UTF_8), ""); - } else { - result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8), - URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8)); - } - } - last = next + 1; - } - return result; - } - - public AuthenticationHandler getAuthHandler() { - if(authHandler == null) { - return new DefaultAuthenticationHandler(); - } - return authHandler; - } - - public boolean registerAuthHandler(AuthenticationHandler authHandler) { - if(this.authHandler != null) { - Grasscutter.getLogger().error(String.format("[Dispatch] Unable to register '%s' authentication handler. \n" + - "The '%s' authentication handler has already been registered", authHandler.getClass().getName(), this.authHandler.getClass().getName())); - return false; - } - this.authHandler = authHandler; - return true; - } - - public void resetAuthHandler() { - this.authHandler = null; - } - - public static class RegionData { - QueryCurrRegionHttpRsp parsedRegionQuery; - String Base64; - - public RegionData(QueryCurrRegionHttpRsp prq, String b64) { - this.parsedRegionQuery = prq; - this.Base64 = b64; - } - - public QueryCurrRegionHttpRsp getParsedRegionQuery() { - return parsedRegionQuery; - } - - public String getBase64() { - return Base64; - } - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java deleted file mode 100644 index 92a2961ea..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package emu.grasscutter.server.dispatch.authentication; - -import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; -import emu.grasscutter.server.dispatch.json.LoginResultJson; -import express.http.Request; -import express.http.Response; - -public interface AuthenticationHandler { - - // This is in case plugins also want some sort of authentication - void handleLogin(Request req, Response res); - void handleRegister(Request req, Response res); - void handleChangePassword(Request req, Response res); - - LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData); -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java deleted file mode 100644 index 67b3d4023..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -package emu.grasscutter.server.dispatch.authentication; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; -import emu.grasscutter.server.dispatch.json.LoginResultJson; -import express.http.Request; -import express.http.Response; - -import static emu.grasscutter.utils.Language.translate; -import static emu.grasscutter.Configuration.*; - -public class DefaultAuthenticationHandler implements AuthenticationHandler { - - @Override - public void handleLogin(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public void handleRegister(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public void handleChangePassword(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) { - LoginResultJson responseData = new LoginResultJson(); - - // Login - Account account = DatabaseHelper.getAccountByName(requestData.account); - - // Check if account exists, else create a new one. - if (account == null) { - // Account doesn't exist, so we can either auto create it if the config value is set. - if (ACCOUNT.autoCreate) { - // This account has been created AUTOMATICALLY. There will be no permissions added. - account = DatabaseHelper.createAccountWithId(requestData.account, 0); - - for (String permission : ACCOUNT.defaultPermissions) { - account.addPermission(permission); - } - - 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(translate("messages.dispatch.account.account_login_create_success", req.ip(), responseData.data.account.uid)); - } else { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.username_create_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", req.ip())); - } - } else { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.username_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", req.ip())); - } - } 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(translate("messages.dispatch.account.login_success", req.ip(), responseData.data.account.uid)); - } - - return responseData; - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java deleted file mode 100644 index b90510367..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package emu.grasscutter.server.dispatch.http; - -import java.io.File; -import java.io.IOException; - -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.utils.FileUtils; -import emu.grasscutter.utils.Utils; -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import static emu.grasscutter.Configuration.*; - -public final class GachaRecordHandler implements HttpContextHandler { - String render_template; - public GachaRecordHandler() { - File template = new File(Utils.toFilePath(DATA("/gacha_records.html"))); - if (template.exists()) { - // Load from cache - render_template = new String(FileUtils.read(template)); - } else { - render_template = "{{REPLACE_RECORD}}"; - } - } - - @Override - public void handle(Request req, Response res) throws IOException { - // Grasscutter.getLogger().info( req.query().toString() ); - String sessionKey = req.query("s"); - int page = 0; - int gachaType = 0; - if (req.query("p") != null) { - page = Integer.parseInt(req.query("p")); - } - - if (req.query("gachaType") != null) { - gachaType = Integer.parseInt(req.query("gachaType")); - } - - Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); - if (account != null) { - String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), page, gachaType).toString(); - // Grasscutter.getLogger().info(records); - String response = render_template.replace("{{REPLACE_RECORD}}", records) - .replace("{{REPLACE_MAXPAGE}}", String.valueOf(DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType))); - - res.send(response); - } else { - res.send("No account found."); - } - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java deleted file mode 100644 index b3497f8d4..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java +++ /dev/null @@ -1,15 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class ComboTokenReqJson { - public int app_id; - public int channel_id; - public String data; - public String device; - public String sign; - - 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 deleted file mode 100644 index 7c49d1278..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java +++ /dev/null @@ -1,17 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class ComboTokenResJson { - public String message; - public int retcode; - public LoginData data = new LoginData(); - - public static class LoginData { - public int account_type = 1; - public boolean heartbeat; - public String combo_id; - public String combo_token; - public String open_id; - public String data = "{\"guest\":false}"; - public String fatigue_remind = null; // ? - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java deleted file mode 100644 index cb3aff349..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java +++ /dev/null @@ -1,7 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginAccountRequestJson { - public String account; - public String password; - public boolean is_crypto; -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java deleted file mode 100644 index 1f4dcd4b4..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java +++ /dev/null @@ -1,38 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginResultJson { - public String message; - public int retcode; - public VerifyData data = new VerifyData(); - - public static class VerifyData { - public VerifyAccountData account = new VerifyAccountData(); - public boolean device_grant_required = false; - public String realname_operation = "NONE"; - public boolean realperson_required = false; - public boolean safe_mobile_required = false; - } - - public static class VerifyAccountData { - public String uid; - public String name = ""; - public String email = ""; - public String mobile = ""; - public String is_email_verify = "0"; - public String realname = ""; - public String identity_card = ""; - public String token; - public String safe_mobile = ""; - public String facebook_name = ""; - public String twitter_name = ""; - public String game_center_name = ""; - public String google_name = ""; - public String apple_name = ""; - public String sony_name = ""; - public String tap_name = ""; - public String country = "US"; - public String reactivate_ticket = ""; - public String area_code = "**"; - public String device_grant_ticket = ""; - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java deleted file mode 100644 index 12fed8f09..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java +++ /dev/null @@ -1,6 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginTokenRequestJson { - public String uid; - public String token; -} diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java index dc0d396a6..5227d9793 100644 --- a/src/main/java/emu/grasscutter/server/http/HttpServer.java +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -162,11 +162,11 @@ public final class HttpServer { - + - + """); diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java index e720a4b15..8b5dbeec7 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -151,8 +151,10 @@ public final class RegionHandler implements Router { // Get region data. String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (request.query().values().size() > 0) - regionData = regions.get(regionName).getBase64(); + if (request.query().values().size() > 0) { + var region = regions.get(regionName); + if(region != null) regionData = region.getBase64(); + } // Invoke event. QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); @@ -183,4 +185,12 @@ public final class RegionHandler implements Router { return this.base64; } } + + /** + * Gets the current region query. + * @return A {@link QueryCurrRegionHttpRsp} object. + */ + public static QueryCurrRegionHttpRsp getCurrentRegion() { + return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null; + } } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java index a64e0552a..794b88ed4 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -1,7 +1,7 @@ package emu.grasscutter.server.http.handlers; import emu.grasscutter.Grasscutter; -import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.Router; import express.Express; import express.http.Request; @@ -21,15 +21,15 @@ import static emu.grasscutter.Configuration.DATA; public final class AnnouncementsHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + express.all("/common/hk4e_global/announcement/api/getAlertAnn", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); // hk4e-api-os.hoyoverse.com express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement); // hk4e-api-os-static.hoyoverse.com express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement); // hk4e-sdk-os.hoyoverse.com - express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); + express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); } private static void getAnnouncement(Request request, Response response) { diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java new file mode 100644 index 000000000..6cb27d90d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java @@ -0,0 +1,71 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.tools.Tools; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; +import io.javalin.http.staticfiles.Location; + +import java.io.File; + +import static emu.grasscutter.Configuration.DATA; + +/** + * Handles all gacha-related HTTP requests. + */ +public final class GachaHandler implements Router { + private final String gachaMappings; + + private static String frontendTemplate = "{{REPLACE_RECORD}}"; + + public GachaHandler() { + this.gachaMappings = Utils.toFilePath(DATA("/gacha_mappings.js")); + if(!(new File(this.gachaMappings).exists())) { + try { + Tools.createGachaMapping(this.gachaMappings); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to create gacha mappings.", exception); + } + } + + var templateFile = new File(DATA("/gacha_records.html")); + if(templateFile.exists()) + frontendTemplate = new String(FileUtils.read(templateFile)); + } + + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/gacha", GachaHandler::gachaRecords); + + express.useStaticFallback("/gacha/mappings", this.gachaMappings, Location.EXTERNAL); + } + + private static void gachaRecords(Request request, Response response) { + var sessionKey = request.query("s"); + + int page = 0, gachaType = 0; + if(request.query("p") != null) + page = Integer.parseInt(request.query("p")); + if(request.query("gachaType") != null) + gachaType = Integer.parseInt(request.query("gachaType")); + + // Get account from session key. + var account = DatabaseHelper.getAccountBySessionKey(sessionKey); + + if(account == null) // Send response. + response.status(404).send("Unable to find account."); + else { + String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), gachaType, page).toString(); + long maxPage = DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType); + + response.send(frontendTemplate + .replace("{{REPLACE_RECORD}}", records) + .replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage))); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java index bb0bc8eea..2de8969d7 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java @@ -1,8 +1,12 @@ package emu.grasscutter.server.http.handlers; -import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.Router; import express.Express; +import express.http.Request; +import express.http.Response; import io.javalin.Javalin; /** @@ -11,37 +15,41 @@ import io.javalin.Javalin; public final class GenericHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // hk4e-sdk-os.hoyoverse.com - express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); + express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); // hk4e-sdk-os.hoyoverse.com // this could be either GET or POST based on the observation of different clients - express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); + express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); // api-account-os.hoyoverse.com - express.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); + express.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); // sdk-os-static.hoyoverse.com - express.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); + express.get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); // hk4e-sdk-os-static.hoyoverse.com - express.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}")); + express.get("/hk4e_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}")); // hk4e-sdk-os-static.hoyoverse.com - express.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}")); + express.get("/hk4e_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}")); // Test api? // abtest-api-data-sg.hoyoverse.com - express.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); - - // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); - // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + express.post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); // log-upload-os.mihoyo.com - express.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - express.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - express.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/log/sdk/upload", new HttpJsonResponse("{\"code\":0}")); + express.all("/sdk/upload", new HttpJsonResponse("{\"code\":0}")); + express.post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}")); // /perf/config/verify?device_id=xxx&platform=x&name=xxx - express.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/perf/config/verify", new HttpJsonResponse("{\"code\":0}")); // webstatic-sea.hoyoverse.com - express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); + express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new HttpJsonResponse("{\"version\":51}")); + + express.get("/status/server", GenericHandler::serverStatus); + } + + private static void serverStatus(Request request, Response response) { + int playerCount = Grasscutter.getGameServer().getPlayers().size(); + String version = GameConstants.VERSION; + + response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}"); } } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java new file mode 100644 index 000000000..943a56d7e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles the legacy authentication system routes. + */ +public final class LegacyAuthHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/authentication/type", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java index 4f52c0826..08025d365 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java @@ -1,8 +1,9 @@ package emu.grasscutter.server.http.handlers; -import emu.grasscutter.server.dispatch.ClientLogHandler; import emu.grasscutter.server.http.Router; import express.Express; +import express.http.Request; +import express.http.Response; import io.javalin.Javalin; /** @@ -11,8 +12,13 @@ import io.javalin.Javalin; public final class LogHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // overseauspider.yuanshen.com - express.post("/log", new ClientLogHandler()); + express.post("/log", LogHandler::log); // log-upload-os.mihoyo.com - express.post("/crash/dataUpload", new ClientLogHandler()); + express.post("/crash/dataUpload", LogHandler::log); + } + + private static void log(Request request, Response response) { + // TODO: Figure out how to dump request body and log to file. + response.send("{\"code\":0}"); } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java similarity index 90% rename from src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java rename to src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java index 8d1164e8d..35ca9b006 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java @@ -1,4 +1,4 @@ -package emu.grasscutter.server.dispatch; +package emu.grasscutter.server.http.objects; import java.io.IOException; import java.util.Arrays; @@ -13,7 +13,7 @@ import express.http.Response; import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.Configuration.*; -public final class DispatchHttpJsonHandler implements HttpContextHandler { +public final class HttpJsonResponse implements HttpContextHandler { private final String response; private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer "/common/hk4e_global/announcement/api/getAlertPic", @@ -28,7 +28,7 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { "/crash/dataUpload" }; - public DispatchHttpJsonHandler(String response) { + public HttpJsonResponse(String response) { this.response = response; } 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 9a21e4143..362493755 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -9,10 +9,12 @@ 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.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.FileUtils; import java.io.File; import java.util.Base64; +import java.util.Objects; import static emu.grasscutter.Configuration.*; @@ -55,7 +57,7 @@ public class PacketPlayerLoginRsp extends BasePacket { info = regionCache.getRegionInfo(); } else { - info = Grasscutter.getDispatchServer().getCurrRegion().getRegionInfo(); + info = Objects.requireNonNull(RegionHandler.getCurrentRegion()).getRegionInfo(); } PlayerLoginRsp p = PlayerLoginRsp.newBuilder() From c7849c0462881d19bc4280beace9cfb1274d402d Mon Sep 17 00:00:00 2001 From: muhammadeko Date: Sat, 14 May 2022 07:50:36 +0700 Subject: [PATCH 333/434] add negative permission check --- src/main/java/emu/grasscutter/game/Account.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 6c3daf61a..39fb8969e 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -144,12 +144,20 @@ public class Account { } public boolean hasPermission(String permission) { - if (this.permissions.contains(permission) || this.permissions.contains("*")) { + if (this.permissions.contains(permission)) { return true; } + if(this.permissions.contains("*") && this.permissions.contains("-"+permission)) { + return false; + } + String[] permissionParts = permission.split("\\."); for (String p : this.permissions) { + if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) { + return false; + } if (permissionMatchesWildcard(p, permissionParts)) { + Grasscutter.getLogger().info("Permission " + p + " matches " + permission); return true; } } From c0d79aa75b85a0492c9b2e6f67a3c8b39524661f Mon Sep 17 00:00:00 2001 From: muhammadeko Date: Sat, 14 May 2022 11:32:33 +0700 Subject: [PATCH 334/434] fix logic and some cleaning --- .../java/emu/grasscutter/game/Account.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 39fb8969e..518bd4786 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -144,24 +144,22 @@ public class Account { } public boolean hasPermission(String permission) { - if (this.permissions.contains(permission)) { - return true; - } - if(this.permissions.contains("*") && this.permissions.contains("-"+permission)) { - return false; - } + + if (this.permissions.contains(permission)) return true; + if(this.permissions.contains("*") && this.permissions.size() == 1) return true; String[] permissionParts = permission.split("\\."); for (String p : this.permissions) { + if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) { + Grasscutter.getLogger().info("Permission " + permission + " denied to " + this.username); return false; } - if (permissionMatchesWildcard(p, permissionParts)) { - Grasscutter.getLogger().info("Permission " + p + " matches " + permission); - return true; - } + + if (permissionMatchesWildcard(p, permissionParts)) return true; } - return false; + + return this.permissions.contains("*"); } public boolean removePermission(String permission) { From f2e9845421f6a9c219b2de083e20d95b6732b770 Mon Sep 17 00:00:00 2001 From: muhammadeko Date: Sat, 14 May 2022 11:34:09 +0700 Subject: [PATCH 335/434] remove log --- src/main/java/emu/grasscutter/game/Account.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/emu/grasscutter/game/Account.java b/src/main/java/emu/grasscutter/game/Account.java index 518bd4786..84873ec61 100644 --- a/src/main/java/emu/grasscutter/game/Account.java +++ b/src/main/java/emu/grasscutter/game/Account.java @@ -150,12 +150,7 @@ public class Account { String[] permissionParts = permission.split("\\."); for (String p : this.permissions) { - - if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) { - Grasscutter.getLogger().info("Permission " + permission + " denied to " + this.username); - return false; - } - + if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false; if (permissionMatchesWildcard(p, permissionParts)) return true; } From 383dfda131b2f79d1c12433e86a171aaffbb4fee Mon Sep 17 00:00:00 2001 From: luluxiaoyu <58876608+luluxiaoyu@users.noreply.github.com> Date: Sat, 14 May 2022 09:30:40 +0800 Subject: [PATCH 336/434] Update zh-TW.json --- src/main/resources/languages/zh-TW.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index 2b6fe34ff..caf37d0d7 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -215,6 +215,14 @@ "success": "座標:%s, %s, %s\n場景ID:%s", "description": "獲取目前所在位置的座標。" }, + "quest": { + "description": "添加或完成任務", + "usage": "quest [任務ID]", + "added": "已添加任務 %s", + "finished": "已完成任務 %s", + "not_found": "未找到任務", + "invalid_id": "無效的任務ID" + }, "reload": { "reload_start": "正在重新加載設定檔。", "reload_done": "重新加載已完成。", From c3ff2b81ec976f34b2ac45513fc1c1d5b2396288 Mon Sep 17 00:00:00 2001 From: zrll_ <46812903+zrll12@users.noreply.github.com> Date: Sat, 14 May 2022 15:12:57 +0800 Subject: [PATCH 337/434] Fix connot execute quest command in console --- .../java/emu/grasscutter/command/commands/QuestCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/command/commands/QuestCommand.java b/src/main/java/emu/grasscutter/command/commands/QuestCommand.java index 70fae0120..affbfa769 100644 --- a/src/main/java/emu/grasscutter/command/commands/QuestCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/QuestCommand.java @@ -37,7 +37,7 @@ public final class QuestCommand implements CommandHandler { switch (cmd) { case "add" -> { - GameQuest quest = sender.getQuestManager().addQuest(questId); + GameQuest quest = targetPlayer.getQuestManager().addQuest(questId); if (quest != null) { CommandHandler.sendMessage(sender, translate(sender, "commands.quest.added", questId)); @@ -47,7 +47,7 @@ public final class QuestCommand implements CommandHandler { CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found")); } case "finish" -> { - GameQuest quest = sender.getQuestManager().getQuestById(questId); + GameQuest quest = targetPlayer.getQuestManager().getQuestById(questId); if (quest == null) { CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found")); From afc2c8bbf2f4078311702ebea98cc7228e94a3f2 Mon Sep 17 00:00:00 2001 From: lsCoding666 <73888354+lsCoding666@users.noreply.github.com> Date: Sat, 14 May 2022 21:28:02 +0800 Subject: [PATCH 338/434] new command join and remove to force join or remove avatars into your current team (#549) * feat:new command "join" and "remove" to force join or remove avatar in your current team * fix:change MaxAvatarsInTeam from 9 to 4 * feat:update & merge branch.Translate fix --- .../command/commands/JoinCommand.java | 46 +++++++++++++++++++ .../command/commands/RemoveCommand.java | 43 +++++++++++++++++ src/main/resources/languages/en-US.json | 9 ++++ src/main/resources/languages/zh-CN.json | 9 ++++ src/main/resources/languages/zh-TW.json | 5 ++ 5 files changed, 112 insertions(+) create mode 100644 src/main/java/emu/grasscutter/command/commands/JoinCommand.java create mode 100644 src/main/java/emu/grasscutter/command/commands/RemoveCommand.java diff --git a/src/main/java/emu/grasscutter/command/commands/JoinCommand.java b/src/main/java/emu/grasscutter/command/commands/JoinCommand.java new file mode 100644 index 000000000..4dac15dd7 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/JoinCommand.java @@ -0,0 +1,46 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketChangeMpTeamAvatarRsp; + +import java.util.ArrayList; +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "join", usage = "join [AvatarIDs] such as\"join 10000038 10000039\"", + description = "commands.join.description", permission = "player.join") +public class JoinCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + List avatarIds = new ArrayList<>(); + for (String arg : args) { + try { + int avatarId = Integer.parseInt(arg); + avatarIds.add(avatarId); + } catch (Exception ignored) { + ignored.printStackTrace(); + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.avatarId")); + return; + } + } + + + for (int i = 0; i < args.size(); i++) { + Avatar avatar = sender.getAvatars().getAvatarById(avatarIds.get(i)); + if (avatar == null || sender.getTeamManager().getCurrentTeamInfo().contains(avatar)) { + CommandHandler.sendMessage(sender, translate("commands.generic.invalid.avatarId")); + return; + } + sender.getTeamManager().getCurrentTeamInfo().addAvatar(avatar); + } + + // Packet + sender.getTeamManager().updateTeamEntities(new PacketChangeMpTeamAvatarRsp(sender, sender.getTeamManager().getCurrentTeamInfo())); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/RemoveCommand.java b/src/main/java/emu/grasscutter/command/commands/RemoveCommand.java new file mode 100644 index 000000000..a40b42698 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/RemoveCommand.java @@ -0,0 +1,43 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketChangeMpTeamAvatarRsp; + +import java.util.ArrayList; +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + +@Command(label = "remove", usage = "remove [indexOfYourTeams] index start from 1", + description = "commands.remove.description", permission = "player.remove") +public class RemoveCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + List avatarIds = new ArrayList<>(); + for (String arg : args) { + try { + int avatarId = Integer.parseInt(arg); + avatarIds.add(avatarId); + } catch (Exception ignored) { + ignored.printStackTrace(); + CommandHandler.sendMessage(sender, translate("commands.remove.invalid_index")); + return; + } + } + + for (int i = 0; i < avatarIds.size(); i++) { + if (avatarIds.get(i) > sender.getTeamManager().getCurrentTeamInfo().getAvatars().size() || avatarIds.get(i) <= 0) { + CommandHandler.sendMessage(sender, translate("commands.remove.invalid_index")); + return; + } + sender.getTeamManager().getCurrentTeamInfo().removeAvatar(avatarIds.get(i) - 1); + } + + // Packet + sender.getTeamManager().updateTeamEntities(new PacketChangeMpTeamAvatarRsp(sender, sender.getTeamManager().getCurrentTeamInfo())); + } +} diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index 02945e6d4..32631db1f 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -176,6 +176,10 @@ "success": "All characters have been healed.", "description": "Heal all characters in your current team." }, + "join": { + "usage": "Usage: join [AvatarIDs] such as\"join 10000038 10000039\"", + "description": "force join avatar into your team" + }, "kick": { "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", "server_kick_player": "Kicking player [%s:%s]", @@ -228,6 +232,11 @@ "reload_done": "Reload complete.", "description": "Reload server config" }, + "remove": { + "usage": "Usage: remove [indexOfYourTeams] index start from 1", + "invalid_index": "index start from 1", + "description": "force remove avatar into your team" + }, "resetConst": { "reset_all": "Reset all avatars' constellations.", "success": "Constellations for %s have been reset. Please relog to see changes.", diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index aaeac5a86..5f379540b 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -176,6 +176,10 @@ "success": "已治疗所有角色。", "description": "治疗当前队伍的角色" }, + "join": { + "usage": "用法:join <角色IDs> 例如\"join 10000038 10000039\"空格分开", + "description": "强制将角色加入到当前队伍中" + }, "kick": { "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出。", "server_kick_player": "正在踢出玩家 [%s:%s]...", @@ -228,6 +232,11 @@ "reload_done": "重载完成。", "description": "重载配置文件和数据" }, + "remove": { + "usage": "用法: remove [indexOfYourTeams] 从1开始", + "invalid_index": "下标从1开始", + "description": "强制移除队内角色" + }, "resetConst": { "reset_all": "重置所有角色的命座。", "success": "已重置 %s 的命座,重新登录后生效。", diff --git a/src/main/resources/languages/zh-TW.json b/src/main/resources/languages/zh-TW.json index caf37d0d7..25f92f869 100644 --- a/src/main/resources/languages/zh-TW.json +++ b/src/main/resources/languages/zh-TW.json @@ -228,6 +228,11 @@ "reload_done": "重新加載已完成。", "description": "重新加載設定檔和數據。" }, + "remove": { + "usage": "用法: remove [indexOfYourTeams] 从1开始", + "invalid_index": "下標從1開始", + "description": "强制移除對内角色" + }, "resetConst": { "reset_all": "重設所有角色的命座。", "success": "已重設 %s 的命座,重新登入後將會生效。", From c8c823b515e73900bd1e93b7faf92708c9a083d5 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:10:43 -0400 Subject: [PATCH 339/434] Fix errors --- src/main/java/emu/grasscutter/Grasscutter.java | 1 + src/main/java/emu/grasscutter/game/gacha/GachaBanner.java | 6 +++--- .../server/packet/send/PacketPlayerLoginRsp.java | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index fa37772ef..327aa174c 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -6,6 +6,7 @@ import java.util.Calendar; import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.command.CommandMap; +import emu.grasscutter.game.managers.StaminaManager.StaminaManager; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 7b498a80f..d3b5d8959 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -140,9 +140,9 @@ public class GachaBanner { + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; - String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + String details = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); 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 db66554f9..52a487d55 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -9,10 +9,13 @@ 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.server.http.dispatch.RegionHandler; +import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.FileUtils; import java.io.File; import java.util.Base64; +import java.util.Objects; import static emu.grasscutter.Configuration.*; From 0405cb6718ddb746157a316cce63f98b1d097f66 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:13:41 -0400 Subject: [PATCH 340/434] JavaDoc Fix --- src/main/java/emu/grasscutter/auth/AuthenticationSystem.java | 1 - src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index 312fdad54..096c4124c 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -54,7 +54,6 @@ public interface AuthenticationSystem { /** * A data container that holds relevant data for authenticating a client. - * Call {@link AuthenticationRequest#builder()} to create a builder. */ @Builder @AllArgsConstructor @Getter class AuthenticationRequest { diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 298d24493..0239b6e09 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -15,7 +15,7 @@ import static emu.grasscutter.utils.Language.translate; public final class DefaultAuthenticators { /** - * Handles the authentication request from the username & password form. + * Handles the authentication request from the username and password form. */ public static class PasswordAuthenticator implements Authenticator { @Override public LoginResultJson authenticate(AuthenticationRequest request) { From 457d00211c225628d6f56f7a3a5b1673509ff6b1 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:21:55 -0400 Subject: [PATCH 341/434] Implement PR #657 --- .../java/emu/grasscutter/Configuration.java | 2 + .../grasscutter/server/http/HttpServer.java | 44 ++++++++++++++----- .../grasscutter/utils/ConfigContainer.java | 8 +++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 52bfa65aa..4cbd0130c 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -1,6 +1,7 @@ package emu.grasscutter; import emu.grasscutter.utils.ConfigContainer; +import emu.grasscutter.utils.ConfigContainer.*; import java.util.Locale; @@ -40,6 +41,7 @@ public final class Configuration extends ConfigContainer { public static final Encryption HTTP_ENCRYPTION = config.server.http.encryption; public static final Policies HTTP_POLICIES = config.server.http.policies; + public static final Files HTTP_STATIC_FILES = config.server.http.files; public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions; public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java index 5227d9793..898a3a17e 100644 --- a/src/main/java/emu/grasscutter/server/http/HttpServer.java +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -2,13 +2,16 @@ package emu.grasscutter.server.http; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; +import emu.grasscutter.utils.FileUtils; import express.Express; +import express.http.MediaType; import io.javalin.Javalin; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import java.io.File; +import java.io.IOException; import static emu.grasscutter.Configuration.*; import static emu.grasscutter.utils.Language.translate; @@ -62,7 +65,7 @@ public final class HttpServer { var sslContextFactory = new SslContextFactory.Server(); var keystoreFile = new File(HTTP_ENCRYPTION.keystore); - if(!keystoreFile.exists()) {; + if(!keystoreFile.exists()) { HTTP_ENCRYPTION.useEncryption = false; HTTP_ENCRYPTION.useInRouting = false; @@ -137,15 +140,25 @@ public final class HttpServer { */ public static class DefaultRequestRouter implements Router { @Override public void applyRoutes(Express express, Javalin handle) { - express.get("/", (req, res) -> res.send(""" - - - - - - %s - - """.formatted(translate("messages.status.welcome")))); + express.get("/", (request, response) -> { + File file = new File(HTTP_STATIC_FILES.errorFile); + if(!file.exists()) + response.send(""" + + + + + + %s + + """.formatted(translate("messages.status.welcome"))); + else { + final var filePath = file.getPath(); + final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); + response.type((fromExtension != null) ? fromExtension.getMIME() : "text/plain") + .send(FileUtils.read(filePath)); + } + }); } } @@ -158,7 +171,10 @@ public final class HttpServer { if(SERVER.debugLevel == ServerDebugMode.MISSING) Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); context.contentType("text/html"); - context.result(""" + + File file = new File(HTTP_STATIC_FILES.errorFile); + if(!file.exists()) + context.result(""" @@ -170,6 +186,12 @@ public final class HttpServer { """); + else { + final var filePath = file.getPath(); + final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); + context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain") + .result(FileUtils.read(filePath)); + } }); } } diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 5a06b90be..b65fb10db 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -17,7 +17,7 @@ import static emu.grasscutter.Grasscutter.config; */ public class ConfigContainer { private static int version() { - return 2; + return 3; } /** @@ -125,6 +125,7 @@ public class ConfigContainer { public Encryption encryption = new Encryption(); public Policies policies = new Policies(); + public Files files = new Files(); } public static class Game { @@ -227,6 +228,11 @@ public class ConfigContainer { public String nickName = "Server"; public String signature = "Welcome to Grasscutter!"; } + + public static class Files { + public String indexFile = "./index.html"; + public String errorFile = "./404.html"; + } /* Objects. */ From 6c419f2d205e8f04c07e764f425e0012cc746cae Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:39:21 -0400 Subject: [PATCH 342/434] External authentication --- .../java/emu/grasscutter/Grasscutter.java | 2 -- .../auth/AuthenticationSystem.java | 20 +++++++++++ .../auth/DefaultAuthentication.java | 6 ++++ .../auth/DefaultAuthenticators.java | 20 +++++++++++ .../auth/ExternalAuthenticator.java | 33 +++++++++++++++++++ .../server/http/dispatch/DispatchHandler.java | 6 ++++ .../http/handlers/LegacyAuthHandler.java | 17 ---------- 7 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java delete mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 327aa174c..bc5144d97 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -35,7 +35,6 @@ import emu.grasscutter.utils.Language; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; -import emu.grasscutter.BuildConfig; import javax.annotation.Nullable; @@ -129,7 +128,6 @@ public final class Grasscutter { httpServer.addRouter(GenericHandler.class); httpServer.addRouter(AnnouncementsHandler.class); httpServer.addRouter(DispatchHandler.class); - httpServer.addRouter(LegacyAuthHandler.class); httpServer.addRouter(GachaHandler.class); // TODO: find a better place? diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index 096c4124c..41aba1c8e 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -2,6 +2,7 @@ package emu.grasscutter.auth; import emu.grasscutter.server.http.objects.*; import express.http.Request; +import express.http.Response; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -52,12 +53,20 @@ public interface AuthenticationSystem { */ Authenticator getSessionKeyAuthenticator(); + /** + * This is the authenticator used for handling external authentication requests. + * @return An authenticator. + */ + ExternalAuthenticator getExternalAuthenticator(); + /** * A data container that holds relevant data for authenticating a client. */ @Builder @AllArgsConstructor @Getter class AuthenticationRequest { private final Request request; + @Nullable private final Response response; + @Nullable private final LoginAccountRequestJson passwordRequest; @Nullable private final LoginTokenRequestJson tokenRequest; @Nullable private final ComboTokenReqJson sessionKeyRequest; @@ -104,4 +113,15 @@ public interface AuthenticationSystem { .sessionKeyData(tokenData) .build(); } + + /** + * Generates an authentication request from a {@link Response} object. + * @param request The Express request. + * @param response the Express response. + * @return An authentication request. + */ + static AuthenticationRequest fromExternalRequest(Request request, Response response) { + return AuthenticationRequest.builder().request(request) + .response(response).build(); + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index b5e853cb0..08958d8e9 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -15,6 +15,7 @@ public final class DefaultAuthentication implements AuthenticationSystem { private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); private final Authenticator tokenAuthenticator = new TokenAuthenticator(); private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); @Override public void createAccount(String username, String password) { @@ -46,4 +47,9 @@ public final class DefaultAuthentication implements AuthenticationSystem { public Authenticator getSessionKeyAuthenticator() { return this.sessionKeyAuthenticator; } + + @Override + public ExternalAuthenticator getExternalAuthenticator() { + return this.externalAuthenticator; + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 0239b6e09..e1d5fddf0 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -158,4 +158,24 @@ public final class DefaultAuthenticators { return response; } } + + /** + * Handles authentication requests from external sources. + */ + public static class ExternalAuthentication implements ExternalAuthenticator { + @Override public void handleLogin(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + + @Override public void handleAccountCreation(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + + @Override public void handlePasswordReset(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + } } diff --git a/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java b/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java new file mode 100644 index 000000000..6bf78af6e --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java @@ -0,0 +1,33 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; + +/** + * Handles authentication via external routes. + */ +public interface ExternalAuthenticator { + + /** + * Called when an external login request is made. + * @param request The authentication request. + */ + void handleLogin(AuthenticationRequest request); + + /** + * Called when an external account creation request is made. + * @param request The authentication request. + * + * For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. + * Use {@link AuthenticationRequest#getResponse()} to get the response body. + */ + void handleAccountCreation(AuthenticationRequest request); + + /** + * Called when an external password reset request is made. + * @param request The authentication request. + * + * For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. + * Use {@link AuthenticationRequest#getResponse()} to get the response body. + */ + void handlePasswordReset(AuthenticationRequest request); +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java index 22a31fe6a..5b012c4c3 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -24,6 +24,12 @@ public final class DispatchHandler implements Router { express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin); // Combo token login (from session key). express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin); + + // External login (from other clients). + express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName())); + express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); } /** diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java deleted file mode 100644 index 943a56d7e..000000000 --- a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package emu.grasscutter.server.http.handlers; - -import emu.grasscutter.server.http.Router; -import express.Express; -import io.javalin.Javalin; - -/** - * Handles the legacy authentication system routes. - */ -public final class LegacyAuthHandler implements Router { - @Override public void applyRoutes(Express express, Javalin handle) { - express.get("/authentication/type", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - } -} \ No newline at end of file From b04b8eef8ee06aab31cc5b0068d7a5337bff64c7 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:41:49 -0400 Subject: [PATCH 343/434] Update routes --- .../server/http/dispatch/DispatchHandler.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java index 5b012c4c3..5f9edcf0a 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -27,9 +27,12 @@ public final class DispatchHandler implements Router { // External login (from other clients). express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName())); - express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/login", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handleLogin(AuthenticationSystem.fromExternalRequest(request, response))); + express.post("/authentication/register", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handleAccountCreation(AuthenticationSystem.fromExternalRequest(request, response))); + express.post("/authentication/change_password", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handlePasswordReset(AuthenticationSystem.fromExternalRequest(request, response))); } /** From f1b12ccf63ef7f8680f944700fd72bb12db88e73 Mon Sep 17 00:00:00 2001 From: tiantian520 <64673335+tiantian520tt@users.noreply.github.com> Date: Sun, 15 May 2022 07:06:48 +0800 Subject: [PATCH 344/434] Add a new command "nostamina" (#877) * Add a new command " nostamina\ * Fix * Fix 2 * Renamed some names. * Update zh-CN.json Fix an existing language expression error. --- .../command/commands/NoStaminaCommand.java | 40 +++++++++++++++++++ .../StaminaManager/StaminaManager.java | 5 ++- .../emu/grasscutter/game/player/Player.java | 10 ++++- src/main/resources/languages/en-US.json | 4 ++ src/main/resources/languages/zh-CN.json | 4 ++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/main/java/emu/grasscutter/command/commands/NoStaminaCommand.java diff --git a/src/main/java/emu/grasscutter/command/commands/NoStaminaCommand.java b/src/main/java/emu/grasscutter/command/commands/NoStaminaCommand.java new file mode 100644 index 000000000..2012bde9f --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/NoStaminaCommand.java @@ -0,0 +1,40 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; + +import java.util.List; + +import static emu.grasscutter.utils.Language.translate; + + +@Command(label = "nostamina", usage = "nostamina [on|off]", permission = "player.nostamina", permissionTargeted = "player.nostamina.others", description = "commands.nostamina.description") +public final class NoStaminaCommand implements CommandHandler { + public static boolean StaminaState = false; + //Temp Value + @Override + public void execute(Player sender, Player targetPlayer, List args) { + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target")); + return; + } + + + if (args.size() == 1) { + switch (args.get(0).toLowerCase()) { + case "on": + StaminaState = true; + break; + case "off": + StaminaState = false; + break; + default: + break; + } + } + targetPlayer.setStamina(StaminaState);//Set + + CommandHandler.sendMessage(sender, translate(sender, "commands.nostamina.success", (StaminaState ? translate(sender, "commands.status.enabled") : translate(sender, "commands.status.disabled")), targetPlayer.getNickname())); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java index 1f452a667..51261ac3f 100644 --- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java @@ -2,6 +2,7 @@ package emu.grasscutter.game.managers.StaminaManager; import ch.qos.logback.classic.Logger; import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.commands.NoStaminaCommand; import emu.grasscutter.data.GameData; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.GameEntity; @@ -297,9 +298,11 @@ public class StaminaManager { // Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) { - if (!GAME_OPTIONS.staminaUsage) { + // Target Player + if (!GAME_OPTIONS.staminaUsage || session.getPlayer().getStamina()) { newStamina = getMaxCharacterStamina(); } + // set stamina if is character stamina if (isCharacterStamina) { player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index f86e09370..23b32c70f 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -121,6 +121,7 @@ public class Player { private int mainCharacterId; private boolean godmode; + private boolean stamina; private boolean moonCard; private Date moonCardStartTime; private int moonCardDuration; @@ -781,7 +782,14 @@ public class Player { } this.save(); } - + public boolean getStamina() { + // Get Stamina + return stamina; + } + public void setStamina(boolean stamina) { + // Set Stamina + this.stamina = stamina; + } public boolean inGodmode() { return godmode; } diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index b4dbed789..90db92f4d 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -173,6 +173,10 @@ "success": "Godmode is now %s for %s.", "description": "Prevents you from taking damage. Defaults to toggle." }, + "nostamina": { + "success": "NoStamina is now %s for %s.", + "description": "Keep your endurance to the maximum." + }, "heal": { "success": "All characters have been healed.", "description": "Heal all characters in your current team." diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index 5f379540b..3f927d2ed 100644 --- a/src/main/resources/languages/zh-CN.json +++ b/src/main/resources/languages/zh-CN.json @@ -145,6 +145,10 @@ "invalid_amount_or_playerId": "无效的数量/玩家ID。", "description": "给予所有物品" }, + "nostamina": { + "success": "NoStamina %s 对于 %s.", + "description": "保持你的体力处于最高状态。" + }, "giveArtifact": { "usage": "用法:giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]", "id_error": "无效的圣遗物ID。", From 0231f26be07923a946277221d700e4287a6127fa Mon Sep 17 00:00:00 2001 From: HotaruYS <105128850+HotaruYS@users.noreply.github.com> Date: Sun, 15 May 2022 05:05:19 +0200 Subject: [PATCH 345/434] Properly handle static assets for announcements (#891) --- data/GameAnnouncement.json | 6 +- data/GameAnnouncementList.json | 19 ++--- .../http/handlers/AnnouncementsHandler.java | 80 ++++++++----------- 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/data/GameAnnouncement.json b/data/GameAnnouncement.json index 96c88f17c..57cd72c8e 100644 --- a/data/GameAnnouncement.json +++ b/data/GameAnnouncement.json @@ -1,19 +1,19 @@ { + "t": "{{SYSTEM_TIME}}", "list": [ { "ann_id": 1, "title": "Welcome to Grasscutter!", "subtitle": "Welcome!", - "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg", + "banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/1.jpg", "content": "

    Hi there!

    First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you!


    〓Discord〓

    https://discord.gg/T5vZU6UyeG

    〓GitHub〓https://github.com/Grasscutters/Grasscutter", - "lang": "en-US" }, { "ann_id": 2, "title": "How to use announcements", "subtitle": "How to use announcements", - "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg", + "banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/2.jpg", "content": "

    Announcement content uses HTML. The specific content of the announcement is stored in the program directory GameAnnouncement.json, while GameAnnouncementList.json stores the announcement list data.

    GameAnnouncement

    ParameterDescription
    ann_idUnique ID
    titleTitle shown at the top of the content
    subtitleShort title shown on the left
    bannerImage to display between content and title
    contentContent body in HTML
    langLanguage code for this entry

    GameAnnouncementList

    If you want to add an announcement, please add the list data in the announcement type corresponding to GameAnnouncementList, and finally add the announcement content in GameAnnouncement.

    ", "lang": "en-US" } diff --git a/data/GameAnnouncementList.json b/data/GameAnnouncementList.json index 7464b3b0f..3697703a3 100644 --- a/data/GameAnnouncementList.json +++ b/data/GameAnnouncementList.json @@ -1,15 +1,14 @@ { - "t": "System.currentTimeMillis()", + "t": "{{SYSTEM_TIME}}", "list": [ { "list": [ { "ann_id": 1, - "title": "Welcome to Grasscutter!", "subtitle": "Welcome!", - "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg", - "tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png", + "banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/1.jpg", + "tag_icon": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/tag_icon.png", "type": 2, "type_label": "System", "lang": "en-US", @@ -22,8 +21,8 @@ "ann_id": 2, "title": "How to use announcements", "subtitle": "How to use announcements", - "banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg", - "tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png", + "banner": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/banner/2.jpg", + "tag_icon": "{{DISPATCH_PUBLIC}}/hk4e/announcement/assets/tag_icon.png", "type": 2, "type_label": "System", "lang": "en-US", @@ -59,11 +58,5 @@ ], "timezone": -5, "alert": false, - "alert_id": 0, - "pic_list": [], - "pic_total": 0, - "pic_type_list": [], - "pic_alert": false, - "pic_alert_id": 0, - "static_sign": "" + "alert_id": 0 } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java index 1b87225e9..c4776a4b4 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -6,6 +6,7 @@ import emu.grasscutter.server.http.Router; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; import express.Express; +import express.http.MediaType; import express.http.Request; import express.http.Response; import io.javalin.Javalin; @@ -13,27 +14,15 @@ import io.javalin.Javalin; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; import java.util.Objects; -import static emu.grasscutter.Configuration.DATA; +import static emu.grasscutter.Configuration.*; /** * Handles requests related to the announcements page. */ public final class AnnouncementsHandler implements Router { - private static String template, swjs, vue; - - public AnnouncementsHandler() { - var templateFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/index.html"))); - var swjsFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/sw.js"))); - var vueFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/vue.min.js"))); - - template = templateFile.exists() ? new String(FileUtils.read(template)) : null; - swjs = swjsFile.exists() ? new String(FileUtils.read(swjs)) : null; - vue = vueFile.exists() ? new String(FileUtils.read(vueFile)) : null; - } - @Override public void applyRoutes(Express express, Javalin handle) { // hk4e-api-os.hoyoverse.com express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); @@ -47,58 +36,57 @@ public final class AnnouncementsHandler implements Router { express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); express.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources); - express.get("/sw.js", AnnouncementsHandler::getPageResources); - express.get("/dora/lib/vue/2.6.11/vue.min.js", AnnouncementsHandler::getPageResources); } private static void getAnnouncement(Request request, Response response) { + String data = ""; if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { - String data = readToString(Paths.get(DATA("GameAnnouncement.json")).toFile()); - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + data + "}"); + data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncement.json")))); } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { - String data = readToString(Paths.get(DATA("GameAnnouncementList.json")).toFile()) - .replace("System.currentTimeMillis()", String.valueOf(System.currentTimeMillis())); - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}"); + data = readToString(new File(Utils.toFilePath(DATA("GameAnnouncementList.json")))); + } else { + response.send("{\"retcode\":404,\"message\":\"Unknown request path\"}"); } + + if (data.isEmpty()) { + response.send("{\"retcode\":500,\"message\":\"Unable to fetch requsted content\"}"); + return; + } + + String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort); + + data = data + .replace("{{DISPATCH_PUBLIC}}", dispatchDomain) + .replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis())); + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}"); } private static void getPageResources(Request request, Response response) { - var path = request.path(); - switch(path) { - case "/sw.js" -> response.send(swjs); - case "/hk4e/announcement/index.html" -> response.send(template); - case "/dora/lib/vue/2.6.11/vue.min.js" -> response.send(vue); - - default -> { - File renderFile = new File(Utils.toFilePath(DATA(path))); - if(!renderFile.exists()) { - Grasscutter.getLogger().info("File not exist: " + path); - return; - } - - String ext = path.substring(path.lastIndexOf(".") + 1); - if ("css".equals(ext)) { - response.type("text/css"); - response.send(FileUtils.read(renderFile)); - } else { - response.send(FileUtils.read(renderFile)); - } - } + String filename = Utils.toFilePath(DATA(request.path())); + File file = new File(filename); + if (file.exists() && file.isFile()) { + MediaType fromExtension = MediaType.getByExtension(filename.substring(filename.lastIndexOf(".") + 1)); + response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream"); + response.send(FileUtils.read(file)); + } else { + Grasscutter.getLogger().warn("File does not exist: " + file); + response.status(404); } } @SuppressWarnings("ResultOfMethodCallIgnored") private static String readToString(File file) { - long length = file.length(); - byte[] content = new byte[(int) length]; + byte[] content = new byte[(int) file.length()]; try { FileInputStream in = new FileInputStream(file); in.read(content); in.close(); } catch (IOException ignored) { - Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); + Grasscutter.getLogger().warn("File does not exist: " + file); } - return new String(content); + return new String(content, StandardCharsets.UTF_8); } } From 213d2883a9a6096b5f435cf364d48c07b1a26c14 Mon Sep 17 00:00:00 2001 From: Hotaru <105128850+HotaruYS@users.noreply.github.com> Date: Sun, 15 May 2022 03:24:34 +0200 Subject: [PATCH 346/434] Move gacha files to separate directory and refactor file serving --- .../details.html} | 2 +- .../records.html} | 19 +++-- .../server/http/handlers/GachaHandler.java | 84 +++++++++++-------- src/main/resources/languages/en-US.json | 3 +- src/main/resources/languages/pl-PL.json | 3 +- src/main/resources/languages/zh-CN.json | 3 +- src/main/resources/languages/zh-TW.json | 3 +- 7 files changed, 63 insertions(+), 54 deletions(-) rename data/{gacha_details.html => gacha/details.html} (98%) rename data/{gacha_records.html => gacha/records.html} (94%) diff --git a/data/gacha_details.html b/data/gacha/details.html similarity index 98% rename from data/gacha_details.html rename to data/gacha/details.html index ccd775ef6..85443d532 100644 --- a/data/gacha_details.html +++ b/data/gacha/details.html @@ -84,7 +84,7 @@ var fiveStarItems = {{FIVE_STARS}}; var fourStarItems = {{FOUR_STARS}}; var threeStarItems = {{THREE_STARS}}; - var lang = "{{LANGUAGE}}"; + var lang = "{{LANGUAGE}}".toLowerCase(); function getNameForId(itemId) { if (mappings[lang] != null && mappings[lang][itemId] != null) { diff --git a/data/gacha_records.html b/data/gacha/records.html similarity index 94% rename from data/gacha_records.html rename to data/gacha/records.html index 7bea40f61..cad1c89d5 100644 --- a/data/gacha_records.html +++ b/data/gacha/records.html @@ -58,7 +58,7 @@

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 sdu>W>qn}EqjlKN0jWnbxzOgPGCD! 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