From bf68d7e98bba3d1501220f0629e2bd2e91cb7939 Mon Sep 17 00:00:00 2001 From: R00tB33rMan <36140389+R00tB33rMan@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:33:29 -0400 Subject: [PATCH] Refactored Folia Support This resupplies Folia support that was reverted in: https://github.com/GemstoneGG/DeluxeMenus/commit/39ecf3a514e774a1a8d1727c4c0b2ca14dc9de14 Notable changes (this should be tested by maintainers for confidence): - https://github.com/HelpChat/DeluxeMenus/issues/289 (Resolves) - https://github.com/HelpChat/DeluxeMenus/issues/285 (Resolves) - Implements DeluxeMenus's Paper compatibility layer slightly more extensively than: https://github.com/HelpChat/DeluxeMenus/pull/228 (minus MiniMessage compatibility layer due to potential convolusion) - Bumps a few dependencies (including PlaceholderAPI) - Hardens dupe mitigation - Introduces open + close command layering - Full Java 21 conversion If Folia issues are encountered and this is partially or completely merged, feel free to @ (ping) me, and I'm happy to investigate. I wasn't aware that reversion was on the table roughly a month ago! --- README.md | 1 - build.gradle.kts | 20 +- gradle/libs.versions.toml | 26 +- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 19 +- gradlew.bat | 38 +- settings.gradle.kts | 2 +- .../extendedclip/deluxemenus/DeluxeMenus.java | 43 ++- .../deluxemenus/action/ClickAction.java | 3 - .../deluxemenus/action/ClickActionTask.java | 323 +++++++--------- .../command/DeluxeMenusCommand.java | 52 +-- .../command/subcommand/DumpCommand.java | 8 +- .../command/subcommand/ExecuteCommand.java | 9 +- .../command/subcommand/HelpCommand.java | 4 +- .../command/subcommand/ListCommand.java | 12 +- .../command/subcommand/MetaCommand.java | 180 ++++----- .../command/subcommand/OpenCommand.java | 10 +- .../command/subcommand/RefreshCommand.java | 12 +- .../command/subcommand/ReloadCommand.java | 18 +- .../deluxemenus/config/DeluxeMenusConfig.java | 172 +++++---- .../deluxemenus/dupe/DupeFixer.java | 47 ++- .../events/DeluxeMenusOpenMenuEvent.java | 1 - .../deluxemenus/hooks/BaseHeadHook.java | 1 - .../hooks/ExecutableBlocksHook.java | 1 - .../hooks/ExecutableItemsHook.java | 1 - .../deluxemenus/hooks/ItemsAdderHook.java | 1 - .../deluxemenus/hooks/MMOItemsHook.java | 7 +- .../deluxemenus/hooks/NamedHeadHook.java | 1 - .../hooks/SimpleItemGeneratorHook.java | 7 +- .../deluxemenus/listener/PlayerListener.java | 68 +++- .../extendedclip/deluxemenus/menu/Menu.java | 135 +++++-- .../deluxemenus/menu/MenuHolder.java | 218 +++++------ .../deluxemenus/menu/MenuItem.java | 130 ++++--- .../menu/command/RegistrableMenuCommand.java | 28 +- .../deluxemenus/menu/options/MenuOptions.java | 26 ++ .../persistentmeta/PersistentMetaHandler.java | 39 +- .../deluxemenus/placeholder/Expansion.java | 6 +- .../requirement/HasItemRequirement.java | 8 +- .../requirement/InputResultRequirement.java | 71 ++-- .../requirement/IsNearRequirement.java | 2 +- .../requirement/IsObjectRequirement.java | 15 +- .../requirement/JavascriptRequirement.java | 6 +- .../requirement/RequirementType.java | 8 +- .../scheduler/UniversalRunnable.java | 176 +++++++++ .../scheduler/UniversalScheduler.java | 42 +++ .../scheduler/bukkit/BukkitScheduledTask.java | 71 ++++ .../scheduler/bukkit/BukkitScheduler.java | 129 +++++++ .../scheduler/folia/FoliaScheduledTask.java | 65 ++++ .../scheduler/folia/FoliaScheduler.java | 220 +++++++++++ .../scheduler/paper/PaperScheduler.java | 41 +++ .../scheduling/schedulers/TaskScheduler.java | 346 ++++++++++++++++++ .../scheduling/tasks/MyScheduledTask.java | 54 +++ .../deluxemenus/scheduler/utils/JavaUtil.java | 12 + .../updatechecker/UpdateChecker.java | 26 +- .../deluxemenus/utils/DebugLevel.java | 2 +- .../deluxemenus/utils/ExpUtils.java | 2 +- .../deluxemenus/utils/ItemUtils.java | 2 +- .../extendedclip/deluxemenus/utils/Pair.java | 14 +- .../deluxemenus/utils/SkullUtils.java | 8 +- .../deluxemenus/utils/StringUtils.java | 1 - .../deluxemenus/utils/VersionHelper.java | 1 - src/main/resources/paper-plugin.yml | 55 +++ src/main/resources/plugin.yml | 3 +- 64 files changed, 2154 insertions(+), 899 deletions(-) create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java create mode 100644 src/main/resources/paper-plugin.yml diff --git a/README.md b/README.md index ac63f750..501b014f 100644 --- a/README.md +++ b/README.md @@ -39,4 +39,3 @@ If you would like to contribute towards DeluxeMenus should you take a look at ou - [CI Server][ci] - [Spigot Page][spigot] - [Plugin Statistics][bstats] - diff --git a/build.gradle.kts b/build.gradle.kts index 6558bf29..5cf53320 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { java - id("com.gradleup.shadow") version("8.3.5") - id("com.github.ben-manes.versions") version("0.51.0") + id("com.gradleup.shadow") version("9.6.1") + id("com.github.ben-manes.versions") version("0.54.0") } // Change to true when releasing @@ -14,18 +14,19 @@ version = "$majorVersion-$minorVersion" repositories { mavenCentral() - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") - maven("https://repo.glaremasters.me/repository/public/") maven("https://nexus.phoenixdevt.fr/repository/maven-public/") maven("https://repo.momirealms.net/releases/") maven("https://repo.nexomc.com/releases/") maven("https://repo.oraxen.com/releases") + maven("https://maven.devs.beer/") + maven("https://repo.papermc.io/repository/maven-public/") maven("https://jitpack.io") } dependencies { - compileOnly(libs.spigot) + compileOnly(libs.paper) compileOnly(libs.vault) compileOnly(libs.authlib) @@ -48,7 +49,7 @@ dependencies { implementation(libs.adventure.minimessage) implementation(libs.bstats) - compileOnly("org.jetbrains:annotations:23.0.0") + compileOnly("org.jetbrains:annotations:26.1.0") } tasks { @@ -59,14 +60,15 @@ tasks { relocate("org.bstats", "com.extendedclip.deluxemenus.libs.bstats") archiveFileName.set("DeluxeMenus-${rootProject.version}.jar") } + java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 disableAutoTargetJvm() } processResources { - filesMatching("plugin.yml") { + filesMatching(listOf("plugin.yml", "paper-plugin.yml")) { expand("version" to rootProject.version) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e810b282..5d782e6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,34 +1,32 @@ [versions] # Compile only -spigot = "1.21.5-R0.1-SNAPSHOT" +paper = "1.21.11-R0.1-SNAPSHOT" vault = "1.7.1" authlib = "1.5.25" headdb = "1.3.2" -craftengine = "26.5" -itemsadder = "3.6.3-beta-14" -nexo = "1.1.0" -oraxen = "1.190.0" +craftengine = "26.7.4" +itemsadder = "4.0.10" +nexo = "1.10.0" +oraxen = "1.212.0" mythiclib = "1.7.1-SNAPSHOT" -mmoitems = "6.10-SNAPSHOT" -papi = "2.11.6" +mmoitems = "6.10.1-SNAPSHOT" +papi = "2.12.3" score = "5.25.3.9" sig = "1.5.0" -bstats = "3.1.0" - +bstats = "3.2.1" # Implementation -nashorn = "15.6" +nashorn = "15.7" adventure-platform = "4.4.1" -adventure-minimessage = "4.24.0" - +adventure-minimessage = "4.26.1" [libraries] # Compile only -spigot = { module = "org.spigotmc:spigot-api", version.ref = "spigot" } +paper = { module = "io.papermc.paper:paper-api", version.ref = "paper" } vault = { module = "com.github.milkbowl:VaultAPI", version.ref = "vault" } authlib = { module = "com.mojang:authlib", version.ref = "authlib" } headdb = { module = "com.arcaniax:HeadDatabase-API", version.ref = "headdb" } craftengine-core = { module = "net.momirealms:craft-engine-core", version.ref = "craftengine"} craftengine-bukkit = { module = "net.momirealms:craft-engine-bukkit", version.ref = "craftengine"} -itemsadder = { module = "com.github.LoneDev6:api-itemsadder", version.ref = "itemsadder" } +itemsadder = { module = "dev.lone:api-itemsadder", version.ref = "itemsadder" } nexo = { module = "com.nexomc:nexo", version.ref = "nexo" } oraxen = { module = "io.th0rgal:oraxen", version.ref = "oraxen" } mythiclib = { module = "io.lumine:MythicLib-dist", version.ref = "mythiclib"} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 40219 zcmXVXQ(&EK({&nKJGO1xwv)!T?cHdHjds-7c4IcSZ8m6Zr+=UK`%mtp>*$`DwPx0= z>0W@Geub=eKm{^SO(BM{p+oNmkXuFgJY7?f*3r%C|pHrnjTh7-ddAF7&H%#N~P zyD#Xi+BB8C9~I7j$N#iPjHkK9zr*ZbvX)iIrtpp*q%?=e;_u9krCnT3RE5@VU37Ax zXDEQmOxms2K+#8&jije%9}!0tl?vvOaz4s4?95IjF`(He$#$2@GX zIet3+(XH!bVa7+=)m?HZ!}BM;$zK_53LR1Ymjajl)N{Pt0#_n<9DVX(h+)4Ye0&M0m##;Yk!M(9DF`Z8 zhvZWAcd5U7Jt)_#5&t0k?{^%)oymcP&~<>C2C4vc^iNc$uj?%7I5E4}D@aau6`M^O z`$#tu8iRU}eb4Tz!j5Q;K3QALb9gVLuW*6dl$c!XB5#84fS#zINU4~{)000wtb6!( zCO==_F5td>Wn$~v*$6NWE^=}Yl3fMy)SVjWi!bEJXzFjK#~V5Ff%4lJnfipqNzZ_r zjDNfeu6%1iWm7J069AYRI~>BUuPNdvT6EUaHQc@!D5HYA>~Jvo7NDpA*XuvO&wzZI+Q!vh{NuNjdUe5Q0HS3m!p z(WsYHmTBWKU;07I6Z9__;V+xX`H@f4wS+Y=2=^u% zSVUQRI54~>DTs+@;>~!Kn9_wH&olU=Cs2Zrkb}yK9pbsL;y9#qd2@xe+z^0cd@TEo zF;crjuG-Q&Zu@x60hw)wNa-&65+TF0lwny{ZO8*_hO$M9v^Oy^S#7ClKm8tDTxpBXQ{*i3gFcn$*Rr1hmi+nK5xJ(F2gA@FiZB8VTT>bG(>w|Zozw%Yf} z$}k+s7lm@7uqWAN8>k%^R80B-9p;>MC133(frw2?4PI^}k|$rK1L`0H}b3DP9)e;GbD^61&xKq`?To(DF)h zIBbP@&*qowowe27M$UpMcrWCiu%fo1$+r7w*RuGktr+NOOf6%=Wlusg{Him;PfvNI zy%0iDA$tpZdlR>CMdLM~#Fhp}J#LG>ddS{We<61$yVnqATN4D;Yc;j{X~~63q$-S- z_gSD`ts5&AlFR~o&^Ff=UnDvj3G)qynf6P#iGv@BQ@EG&b^`glTM7DjD(b~061E2O zIx8y+bDFZjIU#9eAKraUehJgf<^yyXK3&-%opV5;rJu7M^K}r*1)`OIpElj%xji$c z{ZD)(G;S(E%78P=2ncqOeP@YW@uyuslNi^fu3H^X!t*B7%RN`dK zaVzI+(p-TVRhU_^2HqguK*kJOW$0mf`5h@TxP6>t=a@P{v8)MuNZ4TPiX#sCmUPn@ z%@FO#FB;ciUf#Eq{XQY#tHfp|P(7-mSmvd)hnQnQ#tVe> zo6oZ8_J0PR;#AIOb%tF zV*9NB!GbRvx33>P=PEK3&?~)ritIRSsd5H{uW>*5q zHnB^0bMuD;p18VM?!D^l{Qhhb@Cqks6LL@OqfEgE!n1pk&mawYA(5W1WR}W*(>-3d z6l-t*F^4;>a!c@rpUoME#z162l|MYtSOWcM+&Mn+8J$3vvuiKa|CEg=}GSFrVog-_+8gO(pe_Fjd zopb{MUS&VYFZ}AWuq+cV3W2Eec!OrxF%RfFVx0!Gl`bkW1fIbhc`Z3U4$6wFpG%^i za|(M{l2@M`a(pvvi{tW*CB~d%;v9csEbRvsl|9Y<6K*1Vm}YN^G0fa5-_FTzfx;cw z`b;n$bmJLA(jd)9-)Je5w4{-Q5qx1@5%pa^Q15}wXl{{@mLJ8o4UplcFb>AhOpa;( zSJ|nQ!>a&sQ)d_ENTf z6HlAQjguF+zB06KPwDEvvmwwQpK05DVlURe1U{g*R9$J_u>LS2r|TM1=AjleM4s%3 zcPOa>e(Wr4WCV7FSqJjph_a-(G&z)f{jU^-o~ z5j)e*jrI#>%Sa%WAV)%^oe)q9-@j5YJMqagypEYh$YWN+(Vv9|``w;Ju& z>@}m+U!~!}KRk|vuYUX09l!rRc>09j<-@`5@_)Jg(Q+JC`4nfg7#Xqv z(PIf&t{v1IwB)i$J8W|40|TlpHB_k!7d`b`iOon)l)o-igfv0t79n?ZFls$x5gF;W zg&USYPTh`%)ectXbNi?JyF(HqGI$_jLH~`kRRL{s-(Z^~V9ZDr$|X`@$3s`;TLsxl zPs>|scKpDI551T}dQI8RbVK>sJkj#-qkOL|xnMyK{}KU4myq}55%^kLd$l&aX-A;5 z>manmRb7C(@vhcj)VGWoT(Me_-^D(%x+^Q}ZPy0Zyj1>+1Vy_%t@DlRS)J173km_x zn339mwPR8zRJ9W0l2w9DGR1F(6X!|k*+tERniq5~CobZU2lLXxq=)k38q}%cANT7N z1Hz@3e8dD2wzV>Vejp-G9TBR5jD$2&7#EgckjB7Aas{?5*5*%wju?x{?lBTC+Ag)6 z`D+N0dk;&YL7;Gw=@?dk5SzdF;8uL82LXvpC$e26E}4HKPek?98zCPWgs&Z`&b<45HQmKYh^C?U+yyqK#!PEVfXa zv+*nP2ziSk22jHwm@oIYRL@zvYe1C(sq8!6a2bM^VVmm7v;wE}Tg_ie$q>7N9C)g5 zKr2eZ`ejnpbxLSX)xc&B`(${%cHnSASwpNvtDKs+QAL+9l9Bfk-GrEGnV$heREp!9 zAYM@nba)f*aGzG8zE=H4rD2uplA{CDS5aIx`qT`rUSPqGY6@x_f}Hw9V?n3oNfYdr zQ08`c!7WZ0ErZxg#8Rh1RaHPrtP(eok{ekKzm8k9_6QRrpO(FSHMN~D>dYH^*o=my zaCXF+?+ecczjA!cOhdn;8@t77;f}(?9e&{+lE&NdjWkJ#(_a0wyFzyJB?=_GrH)}(PVnhvmu|IVHT3W8bNTSa>a{p`N@^;_ zoAx<0y64$`U=T~jgo-${@<7M|A9qy+3I~>OBxS4K$funriZGC78(0QC*d!PAKhl-| z21XR$UKsWflOk>|joXB_-X`tV1XB1i=3$Z0QbyIhZy)ZcE9klkW5wi%n)G}ljxA4k zU@`TvMSbF#F>~=neWUPL-Pz3==<|V4T_7i#p6Hc3<)}TD_SDGRcsZaKrzLbYb|ShaAmZWp^V-FJAI5iL=BdbVF02+0wg)}SLG zb3cji74_Bp>VF&xzi%8%zYt9yI(gnIt*`1kN9r@s)5T?K5fNd0D8I)k8)SW}1d#YloEP3Z>95S}ACcXkp%jaxb*Qo77Cv z1t{8i@6(@`{szPFmoSyoaO@hQGJQ4{u>|pfA-;-DYuiZAT$UDbS1;;BF>dSQu72e%dN>|@Okf8Qe@aHQZ{lNJYf_K>%V%zBDq?pQ? zvhR=Vx_UvIwXC`ZifxLRv-2sYmc6{ycr?98Qgd1n8R4@=@wJ)szOOSn?Z;{_z%T#N zu$cdFuQ411ga8@@gqk80co|y=9G6E0rXxoNrc$s;57IV;dZSq(mLuVP$5d8AkK@Gi z^I6&UN`3UU6U4MQ412~8EQp>S#0`cJO}T0EghfG+$hzOm{hYbV&JDPnG1&n^=z+$r z&0Mae4|2Bmm*W##dI}Akx=Rg0cD#g1IzzZVe&HOVMbP?EomfQ5mM&o|t1RA829-896AsrqlPvVyTsS)*`r*Zgc69Mkra_o z7H~RW-yreA){*@SsB_zLP1^wBDq9Hc7ttNR+aH>LJ+(KjMBiseCcz))wyR5xPC0A5 z4$RsO3*jJw@a<)XFSD2^W)jgf5C?97uSpGW2x#a!^q;wmThAf`Mh8^iZgB?cZ<@MF z#3M>2`Z7R}G176S*=GI6E(W2+W?es)_^qmY_d2^S-A)&?+~zlyZh6AYg`&ofB$~f1 zU_I4I^pBWhG5ThjyWcI%p?sZ=A2WDBa88>IZoBI5=VO(np|@8_;bDANHni)`ZW)3 z7n;M_1A#PI|9V*|or?iHx$*M%B&u^IiDKQi)CMfk@J*NhQp}#bMo|$QMCl0BJW~OP zV|{`k6%{eYmBq9S#SPX=3`-2dwL(GAicnIaz)?>@M_lh#OYc~8O0<2>XGxTFDFErO zNRC`5@d@fv=SI%AoU6cN4x8Y=FE>cPsBrK_^0KSTL@N1fuaIc#rYm1-Gu0jngG}`| zIb-9kaqmK1>6!YB#D^h$xVwR5_d%_eoqx_*S`Cxb(2%E&;4)7g;MqmE`!okrD;WQ7 zC0k+a;1h9{OlHZepiYsZtCLG66FqI59f)Yu83VJrN%3botGm> zK4wl@WaezwYI3?f)Rm{7C}BL@T@T6gc!f$&9|esF7v$%Xt1rA{%2EO)e}3lm=htqd z(=FU-b9f+sH}3OtUPj$gSRc^u;37x(2YbHendv#nouCpu@7a|6p>Gc zDFX=YsR%2WIvU{@k5H(aX9Lxq{2>uNl#dmZaUZywv0{156^~}=V0WV04u3oJ0TUP<{(hU=)uph2MYc) zNTrig%V<778rQd`1qXL_ft6jHS?X-?{Hx}{{phk<>Bk7CYB4}cIoJZTd;J4Od^$WE zTaNHnOSkmO^Qhs$Ij4HArHctndtK2p=Ysri+Icgu6 zMqVJDofP^tesW&FX-v;20$a!s0jYO#=yi(EY)niD%D*_TJ;lAjivWqEy^D)ye0?Zf zTh;7>JHmI`P!=0c6An_a4sne_SNAayTJLO{SxC?KrdJo}geWa?8~H_AG*p+o!aFR< z8;`+OmAO}93xnf~?bqlZ7iJtrrk`h`B_i|o`9X<+266@9F zj+46dPeM?D`QJcuN+Sn!0kESJ^O{gQE!?W}Jv8no<~!qHCh9OtgepWSs2gA`LbZ-v zS{^K)8Le%XYe)m(|AuOokUW2RFOPA9w|9XjeILG>5}KK;+sMj;xF*O&cn4D}0 z&{4LL*^dkXe{1q|6xhvd@Op0P1;Cl3ayjgRv=%R{M+cwvq>=PfVJ=l<&`l%KR+z7J z*a&!7G_f3E6@_%_90GK=L|y{sWdN?U$-9I3r}txa?~`&EuP+8Rd?RuN(}f3P_|&y9 z%3r2hH58vz#A<*Wf<}?rzOx{lCYZ&}$nFImuE0%tt~Wd%DcsBMG3c)IN14Bl2gXou zLXrjTP2=){Ur0NOI-Q57Sxx`;5)0zDtJ+=}`i0lUCbTtb9r}KKfHLm-rtIRb^&7xp z;#wHBGPleAVHbL>dDlHv7a14YaI;Ua9PAr3F>$8o7wN`oMepMK_18iP+{pQQh_W72 zR*sNJ=m=8azTb4^O*dro&VO<1T<`}Je1U)ng8ctdqa@G;QyfcJGqI#iE<~qEyK2Ke zq!GP_rkGn~b+F9-dj>%u`v3>i#-*+`u5sMQe|q>*aY;1ICj>CX->2XPYLW`fX>%$3 zH2vf4VmCC7}4NbY}P6d zu44{&7^u$X3hC52av`?JwX;Zkd8SiLKG+Rs&?Vo{=$E@gxXObeo_FR`guc>bNM8Iq za}YHGxg6(a>Q$$^!e>Y=ARX3Xw5cnEYY;GjgWN$o1)}#`!9f{lA?VCKaBc0BRhVf< z4maF+D#|r5E3P6Nt9KoZ>@xFMaXj}yjlf`#1F}=Kap5Bd^sN6B|5gd=kHTA;?uPiQ+$8%GyD}AGl_?dH($UzZK}tlYwg8Wc4lYzS@m!S zXGAU;nmEDcYZVa$D#5SOHm=eeR)E!Y`(AK=^>Gkl(emy8uDXEU&{hP-T>s zlVqwOdag2$@KJe*m(h_7v&ABXH<}!s#C({;%8N&Q_pGwf6&3Y&Fu=IoN_sjAy4!Nu3fnh&WYbj?wl9I=?< z5k`zi>fryiPG33*3CRByy!HQS)NYJoz@!-!LU#1wk#H)mM9lnO(G!)C#p8|pc}<~} zaOkNF(%|Q=T3cc@1;D2M|wbokr5PmIk{-eI+cIhUC7aF^)y~NO>Duk4DlwiPm4Cm28apZV{Bo zSbw(|fUkG+t5p2L<)zIDvi1*U5TYU+4zcTEGXhe}YPCkcbh@9%UX7vk@Ep1B*%?vW-M zRN4Tt?~af-MthbS(w4mz#Zd_3wko{iqBigylZdgPUdBFqktYy5}JY8%_ynNG2 zhaF&;YGu4^&Ie(p{^!1o!+nnGI3v=}3pLW27t#fO<{Lh!650QXfB92UrV4Dz>;uFa zM@)0U#?oDE@$7^rV8jDZu@eyf+bl#yw{LYbA-&CQZWRuXhCL&H&K8^Qqy5*DOfyRp zP}x2IL!oawmk9%5%j$yY(>Hd@<^GPQLBI+C8BO9eBmS_3R%yax z>i6uEpQ&!tDNK4&;zC{67Fi%^^>WI6(W?HWMUqUiSuN!|vLUY|g9eLa1#mtoAEje7 zad+MfuloF2*x6smb+TDcS*b$g40<9z&)WHR)#)AjH24J8`8`Vf@YCj8$yMa$#S3J4 z`8C)`Qk2CyQAS4+HmF<9OfC$BG)_1-8)p3bJNvyLMeukL_32^|#voTPORxMM8<_%r zT@>0)^7@D)3vgT7y8oMkJuo*a0wk|wS$?o&{an0pz}ez{DWjRQoB>jyoTE!7ZH>p1 ziPvATSm8`BJ|tyy#$vV47@jiT{KX6^;6$7stpE(g#0 z_kZXy0{wr3S0p7a*chDzoWKYRj%ULF8tDRL@q&k;c?b2V%eio@vJ?mltE7H4fT550{g`2xCm4Q+~S;d{L-&N>BQ~YZ= ze#bv-w*9sfJ`VCswjubwOM`3@NL4LKhHct7Xpy-^&~>d3y4{AVm$`BTGwq6jm8A@$ z&Im@X2D9c78}uao(jY|c7VU_1LY zX^o=PmyyYtrs%+4#(F$C(vM0eKogphn3v}wbUev6+ht~H!v)VivMe2bqn5VH*rq~< z)9kM`?vljRrEbn^{mJN-e7m z%7^&oMI>#Pw*KpwRt*^>$MKX*7Kz^3yF6|k6sx-2Su-aRR;E69s2SWd{#F$yZM1PkONXz7QcxN+3xBCs0ulYRrfv z6rnNBU1R|9Y~j-jOF^MfS-exHht=VD0~p$?nbeEt9e2`^f?gYfmnzU#1;TMfd^qBG zZ-(QjpvS>2MU$->;mJQT-LJOx7AcuM>F20>L7)~R%jD}XVTWZ%Fm$PSTb^Sp*7X6p z(Ud2Hk!IOqy3piYoztE`uuxAMvJF7<`>OZM7ZbDjpgeN|)D`3aDJFKJ?LR4b7-6-T z^!M%irSzp0=qjJIZtn|J_nNTlsI5eAEk{}{ZZ(}!hBnu38Hj(FqSb$+YDW&u_F`vM zxmMAUEJhSW!6wJOWXUCa;JH%cJLbO-$n{5jw>XDqY5&q4HGzBsoH_t`8Kt!6Hjh74 zvSk-YOZ)E2we_>59)QdQICQnwa1Ad1G<8}!Pi*S7(p}EZrXO-MTh*>GWH*M|>m*@! zpz6}a@GKQ?rvAA#UN8zGH!MM)&ct~cajgw8)I=$40ATlV2ktBQ2jR|jkH2um5y{}e zeja}}jKfv&3+X=vzAB?8uzO;)IL_OGoe^qT#ws&Px-<~9^6i}y<1wAPnwhbewODIy z(s8G&t!}6*m$Rs_TV=VkivK8`v&6NIMI3LGLB|HYrvW=i~js9|_o9n)t#?Ufr;{)sK)Tq=eg+T`YTZX6!fBU`6XNNvH=M!Zn z>LqCvuYXp&F;fxIp+3G+#o2$$WLv;OHMhbZm9dfXEsVWYGr45QssG;EcO(nWR%$9M z*DI6w=}3$V_*Gy0o*Ok)M#q)!1+X6c?G0&f3&j!c(RX#(c7Y=vt~dA#KLIde5Q!@2 z8gQS#0{0V-e^II>HJ29`5;vh47|9A29#KJoHYsGrF&vhiaizqG%hi`T!k-tVI3<{2 zrF_Gh+JRoZTM{*))J6P6tl+|of{C3gIAlbwGreyGIz7I#Jl-h3Sypf@-y!D~dK1+b~kOeB8F04%Cbo>-(f@?h83 zb>=|An3+^c5LLF)zp`vGscEMK<@9_lm}W(l9CwnrRE1EWNCI|E{=-BZJ}BSQe|R_# z4*{OXCj5to05F*V1~`-ZA0BD~Y_NijF<0xMxzw?mQ}Cq8NQ=MCk4ECq;ZT2ZsOC}- zO*>T9TRkuBB$O%BEL#Ao|NYj=Cn=s~cc#rM@B!6*K64_SmFojGowr;+GT-@WGc)3` zeU}Y3`F#D9h0s+u^?kfQ?S4-0W2XK&@!;r6lQ9NE*%zlySD9&E1q6{W<9$`}2Rj%x z@C9w^RNGo-*?8Az^dU|kVomKQnX;2I(+!m~n7fMjIVnVgYw+vdjukRx5FHGnRp0@_ zb$_z6DsyQsNO+mYrC*<)7kz~?e+Tr{3H{s2j??|X4^~{_ll;YY*U(jA$xSk0YkyvA zt}oPSiG>)GtE?u30CWa{1;=wS7qEm$4R+S|@C_tz2M~(r#v5?*iXD?u_q6)inbs2tMiQa)^eL#~}`fHThirV4@<4XcAG3t!-P1@1ldhg~{*?mNa}!+>2? zhW*T_i)8h>e@C=3bSrx`htQIf9TZ-lNaxje4(_TuIWx4FAr4xgB)Y8J|1?HEh#BST z-*x$5F^dzYSgxMK=3f%Nr-+k7lZfJI^O6nfAn7b{!{P_|gl|bX+!3xDJB5bRgBbrx zCv{wK<1g&H1Ahwv7tTIQA<;mlq8$fsB1Do ze=^?;k&cFhi^C^^SaBMt0C%>irS_D90K688kfWE%KbetgCqmB|-;R-g=Bvrpwto3M z*)edBJ0T|-%NU`FsMZyiMK#DBw&)fdeSGL>vCzw?u>+8MY# zK(52c1io^qDP!Z|x5z+tn87s)$wD+9kQ_p{Z{+*nOtm2Y0}8ijZ*EDK@zP!>ea{SlZ{()Zs+}DXqUk0y8w|w8E=AXW!$IdKDfRf8I^5pCn?#Miy|vJj!_zWfR-6W3l(OKIPc*9H#600&8M!@c@y2Do11* z6>qtMmN=i~P^&ghtl}l3@#%R_Ewbi=c()X-lcWHg*>Gjg7NzG!22Ii)&4La3H=9D8t~v7ejT8i zvf_XkdXU4?qQ2;#ktIpg!V-2{?9dppP+TNzn1T@}4&~M@Dl1<4s8!Pa-fObZP|;tc z)GA2ZUC1um0e99uJwTBj2qFu`T`P`uq}BBO%S2=n4H~@DyD~ICW<2z zyW}*zbH%fWAjHTPx$1y*@VdqT@et6MLN+(k8l5C$>_mUF^Ma&;E z%+n4DrK;X+to0&^7N-tBhJ!l=^Vxjr>FW8?I>;F7w=z$qYny;}aeOuC{R!q!hU${eF7(ZFxZ(D6sY+eV$ zr=Jc<>a__7Rp|IpCqr^Vg&V1rX6`ffH9)B$7WibreymOGVkQsXg7gTHqh$tX%luJg z-9gGFjl%va1W9xF29Ft*ZPgZ)zuCYB>r5)w384nbOg-uW%zpN2O!<+B0}ZC+Ct4rs zHvkMfNqcQ5JG6)Q)Z7rl02VUBzn)tGl{JIHN-x)y$+zE}DjfqEP63-|^E!P{$|I_l@ZNMF$ zhY2~8#eWmzH43wZcL?*mF4@dR=1NRJ@CQz0rQq>wI zeY`D;{Jpv;wEKQE`P!0v!F%fVox)P$WaLxjd;mf+fNO!ugDpy!PfD7tJlzr&XZB{m z%KEp|F?AQyR>GuokU|}qaz$Yu9;R+Ldc1`nZTuc|M5ElVdWL^<$CM1!q;67@cO3@Q zjB}}7!$n})PrQ_y$P@l9HK5+V#iG>L8%vWuKKgI zZ0FU^s<@tlaU_iFzwzD>o4lSvabVl`35?E zE@xP&>SZ#0=h?A<%0v{7QtyTHZC2>R_b+4?aQN508B59gr~GEe8U#|G68eF5G0L>6 z;3o`z%~{Gh@BDFILDv20sCtk|_p0ACQCd10$=e7@lpxr<>Ad&PCTRDy6UU}(qg~4C-~thtgbFLQ3PIpPO;F9 zI8X+;qU4Boh4~y;e*j@8*9Fo&0>c=B(!9U&b?V7#Z4bN=w?(7D&;9)Q816dXG?wSs z9s4$gNx}<^>t}jVSc(}wFsO5b6`|J9-Wb=5rYaoKt z9<|yDTBKRG;kWxG85{H*s+a7+)&{IE$)Chk7DHH{Z3(G9=rON=SBaiO|mr0#^IA$rCW*k6;j7P)4b9cSx*Lb{GknAG8B?wu4|c;0Q29Xl!7bNtQ~>qPr*KLj z5BfyJMGu^7*`Dw%_!cII@F&w1e=U2RZ# zfrpz4)GkGu)s2u4QX4L*Qrk>^_x}hA_=)q#)Xy)Oetpx+|?->NdckL|?fHv0OEgCa0*KD8~ zgNeR{(}ME+X!@c?Wm5|tm`6^ZO`17tO1Yzc4J8X@)ogdES3J<$cdtMoF*FWU)oU0y zLQ6d2e@XL!iu%&^wCOQ@mA&)NtV;U#^Oo@2&w4x(VPK%I*L`qPd3kHQdQ80Zk~^u=4 z8GLFZ2hW9`%~>zZ#q`ue`T}z;t}qWpP6n#(i5da3q(INZ$x++J!7CY3-#)x|zn^9m zfiesHlEBR-hKiVy6wh4y)y&*^ikZzLwMeFH@P7g&qjBc$>N&?Jvxj zKH13^8@?lv;1ipZmZ@sL1P00O0XDpix-AT$m?!9G2+5u8g)vZRdKQKol-J z2)K=k+=58{#vabMq`?)fZZ7Aou|;c7sm>=K2aih+0yLmHUT6tXW2tibI{ow-b>&-T zANk2g>nUo<_bIBK%BDc97;?tA@qTe~V&v8;{UvTsf>)%U?{{@3sB$&H(Iks=O=Z`j z?}OHI;)?5p;iAAB;FFYG?HZt<`==Ol4VYR?NoA5}u5TEM51RFkXNL2X6y5NsTF(_U zgJFxF`H-WOEawVIE3>WU(`8n!p@S|=b@60VhD#j4HhYDUZU?GuK2fOVaEatiD9tGQ zHi&WN%gpU?)@T6NnqIrxRE{&%_jiuQp>Y(E@?e+{)129!|9m&^Hu^)^;MqZ00W>)v zEa;(X3(NsgaK#|s@@S`r+ekOtq^^q@S1o(y*6I+pXe_=oiBn2RoPOu`td&?pLq+6j z{7xR&nq#Ns!C(ifJHJ@3neIU8CTvxsrqHM6%V<5_6IVHs)?@&({~PC%s1 zhew|7&X;quI_#j?W7~NK$$rz4#gD<< zrjWl?AM46Rgep#V9=sg4y$8jb$38*>zHDfxm^B>IhYmTe>8?D?ER6y`fu#|1OLBO zEt9wsFj>)MK@2NmX10l?!T`}}{LPAoevn%&u^P4^A0D9sySyNuiFW!s=f!iu4Hcmj z5WcstN~;jlfGQZvHAhVh20^ifd*rzDDfcGuOe%N3^=fXsJrql{B#E z&`e5)2>wOmA~|u#<~GF0Xu%)Z4XVmRwEweDP@7$`z<*$sKeJV9$~aO7es3vhYl zB1*wM?q}M8Z1sIW##Xx75F9q_><+quKbFyZ4K1-)T=yZQG!*Dx!f+zP^hmM)x`$jE{LhaD2k&}o6}0}3;zs_D3C$(tf!T@BwMcM+xBl3W^95y{_El0Rh3JuH6YG_=lK)^3r!J!M^f1ltaR``mDh$LJ-khhBZhGg*(&;guRL{6fAyz#QzHo3FN=7k`dhF1P^v8K}arP0e;tv+Fjb-WFTSB{tPq6)heL_V%^Y5)p#3Wc$w z{}%rhSDodZsB0=L@0{=Ab?NzW)%`Tp<@++h31L$|di@aw$GBAYo!i+Neue`F`KQw) zUg^#SKaeGXXot}!NKs7)y>_=Ce+U(A!df2q4oB2#Mj6^v-$m z8~UAV^wrn^*SgsJo7t}iCj)3?@OLzHuH+$tfqZG^dOxXX!)NsCzt$467gsQaSI=(G zsFdY$tmdX({Q(! z$Uy!x6a&tEbs`q;ao*>wV|Cdf^1?s2Zle1&^# zrIwpFo`J9a!S|%h&N3Rt*dut0B0d%ko268U59S9u8JY2rF$D2NgRZ-x4m#|TTS*uR z^CDkYy-=|MTtm7I*GlRe%C7X{cig(@V?ZVbf^j~1s)(V4d=IY z`@=9bx3s<8w;IL`RVSfNHxwc%+HXLt?gv809VhAJsgi{=LQofh^4u4l@IyS_=FT?>E_vVSo+3)SQ1Jr!0S&?WlwUFz<-y!|FcsXq`@Fk_{!ogIu+E!q$8G#VX%skZJHm1;kQV3 zHg47YG)TnzAQyQ);Ur*I2ONNDKIv|la2Ip{cJpg%9z7n@KGT6eMS($|h`S7>_{P2d z+5b*>)Dd|!j)cy%AXY@YosS~gk5UnHMViK) zRFO}H47+~Cx-pKD52iYV2}$ey5Qs8iUgN><5=t;ZB~zJp4^K=3Q=WAsk}OdKFtBIH z4gEAX0^-A3sNrrVa}5<+A%VgfJ;t&RzZy{SQnBJhD5ox+QLJ#es$^3BR+MZ}W-5vb^k?6lDNob0kThHp${ZA;WZG|@XX zYqt1YIDt$~*-`zUZ&y4P#Fm*k`;7;5z`r-+HnZVs)l$gSJT~hfRrdG#l{bn(mTsWS zR|k^>!+4Wv9y)IPS0(7RGI4+)KA&^fGo|2)4FjXvQW&z@aP zp=i^=rMe!Yl}wGar+T0yRv9T?bx+4PGE0Q4)Y0S6!6SJrUO$y%=-!8xB7xvPS0*vy z2fEb1@*QjZ8LIMFhzP~(=^i4H%_O<`EV#6@_HpT@ox6rs%2KiII^3|9jA25ItOXWW;gc244I(n+ivA_iD za~MRML5Qjg5v8TLfv?tTy|IBqIEOA|yuWN^eSqJ*y_$6Al+Gmm&kCOe{3{zKdGy~z zDQ;2w>V!wT68ysa!%yfftUS9%Dq8ivTaEJ>&M9U{dk{RKNoO+@)n+0h@2(-!}^ zMv?sS>i*fAt~$fAt}�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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 r1`0QNfccvgf>;njKhA^zKy!_#L6FRI>Tmx6$J /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4..8508ef68 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,16 +13,18 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -49,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -63,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index a91e4ac4..b9a1575b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,3 @@ rootProject.name = "DeluxeMenus" -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") \ No newline at end of file +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java index a5b4b354..12d79301 100644 --- a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java +++ b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java @@ -15,6 +15,8 @@ import com.extendedclip.deluxemenus.nbt.NbtProvider; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; import com.extendedclip.deluxemenus.placeholder.Expansion; +import com.extendedclip.deluxemenus.scheduler.UniversalScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.updatechecker.UpdateChecker; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; @@ -28,14 +30,23 @@ import org.bstats.charts.SingleLineChart; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -59,6 +70,9 @@ public class DeluxeMenus extends JavaPlugin { private final GeneralConfig generalConfig = new GeneralConfig(this); private DeluxeMenusConfig menuConfig; + @NotNull + private final TaskScheduler scheduler = UniversalScheduler.getScheduler(this); + @Override public void onLoad() { if (NbtProvider.isAvailable()) { @@ -113,7 +127,7 @@ public void onEnable() { public void onDisable() { Bukkit.getMessenger().unregisterOutgoingPluginChannel(this, "BungeeCord"); - Bukkit.getScheduler().cancelTasks(this); + scheduler.cancelTasks(this); if (this.audiences != null) { this.audiences.close(); @@ -217,6 +231,10 @@ public GeneralConfig getGeneralConfig() { return generalConfig; } + public @NotNull TaskScheduler getScheduler() { + return scheduler; + } + private boolean hookIntoPlaceholderAPI() { final boolean canHook = Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null; if (!canHook) { @@ -338,7 +356,7 @@ private void setUpMetrics() { .map(Menu::getMenuItems) .flatMap(c -> c.values().stream().map(TreeMap::values).flatMap(Collection::stream)) .map(MenuItem::options) - .collect(Collectors.toList()); + .toList(); results.put("Byte", options.stream().filter(option -> option.nbtByte().isPresent()).mapToInt(b -> 1).sum()); results.put("Bytes", options.stream().filter(option -> !option.nbtBytes().isEmpty()).mapToInt(b -> 1).sum()); results.put("Short", options.stream().filter(option -> option.nbtShort().isPresent()).mapToInt(s -> 1).sum()); @@ -351,4 +369,23 @@ private void setUpMetrics() { return results; })); } + + public boolean isDupeProtectionFlagged(@NotNull ItemStack item) { + if (!item.hasItemMeta()) return false; + + return item.getItemMeta().getPersistentDataContainer() + .has(new NamespacedKey(this, "deluxemenus.item.dupeprotection"), PersistentDataType.BYTE); + } + + public void markDupeProtection(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + + meta.getPersistentDataContainer().set( + new NamespacedKey(this, "deluxemenus.item.dupeprotection"), + PersistentDataType.BYTE, + (byte) 1 + ); + item.setItemMeta(meta); + } } diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java index 59d61685..aefda3e0 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java @@ -76,7 +76,6 @@ public void setDelay(@Nullable final String delay) { this.delay = delay; } - /** * Get the unparsed chance of this action. * @@ -102,7 +101,6 @@ public void setChance(@Nullable final String chance) { * @param holder the holder to parse placeholders in the delay for. * @return the parsed delay */ - @SuppressWarnings("UnstableApiUsage") public long getDelay(@NotNull final MenuHolder holder) { if (delay == null || delay.isEmpty()) { return 0; @@ -119,7 +117,6 @@ public long getDelay(@NotNull final MenuHolder holder) { * @param holder the holder to parse placeholders in the chance for. * @return true if the chance has passed, false otherwise */ - @SuppressWarnings("UnstableApiUsage") public boolean checkChance(@NotNull final MenuHolder holder) { if (chance == null) { return true; diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index 6d728108..e763168f 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -4,22 +4,34 @@ import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; -import com.extendedclip.deluxemenus.utils.*; +import com.extendedclip.deluxemenus.scheduler.UniversalRunnable; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.utils.AdventureUtils; +import com.extendedclip.deluxemenus.utils.DebugLevel; +import com.extendedclip.deluxemenus.utils.ExpUtils; +import com.extendedclip.deluxemenus.utils.SoundUtils; +import com.extendedclip.deluxemenus.utils.StringUtils; +import com.extendedclip.deluxemenus.utils.VersionHelper; import net.kyori.adventure.text.minimessage.MiniMessage; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.logging.Level; -public class ClickActionTask extends BukkitRunnable { +public class ClickActionTask extends UniversalRunnable { private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final UUID uuid; private final ActionType actionType; private final String exec; @@ -38,6 +50,7 @@ public ClickActionTask( final boolean parsePlaceholdersAfterArguments ) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.uuid = uuid; this.actionType = actionType; this.exec = exec; @@ -58,7 +71,6 @@ public void run() { ? holder.get().getPlaceholderPlayer() : player; - final String executable = StringUtils.replacePlaceholdersAndArguments( this.exec, this.arguments, @@ -67,66 +79,37 @@ public void run() { this.parsePlaceholdersAfterArguments); switch (actionType) { - case META: + case META -> { if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); break; } final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); switch (result) { - case INVALID_SYNTAX: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); - break; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); - break; - case INVALID_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); - break; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); - break; - case VALUE_NOT_FOUND: - case SUCCESS: - default: - break; + case INVALID_SYNTAX -> + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); + case NEW_VALUE_IS_DIFFERENT_TYPE -> + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); + case INVALID_TYPE -> + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); + default -> { + } } - break; - - case PLAYER: - case PLAYER_COMMAND_EVENT: - player.chat("/" + executable); - break; - - case PLACEHOLDER: - holder.ifPresent(it -> it.setPlaceholders(executable)); - break; - - case CHAT: - player.chat(executable); - break; - - case CONSOLE: - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable); - break; - - case MINI_MESSAGE: - plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; - - case MINI_BROADCAST: - plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; - - case MESSAGE: - player.sendMessage(StringUtils.color(executable)); - break; - - case ACTION_BAR: - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(StringUtils.color(executable))); - break; - - case LOG: + } + case PLAYER, PLAYER_COMMAND_EVENT -> player.chat("/" + executable); + case PLACEHOLDER -> holder.ifPresent(it -> it.setPlaceholders(executable)); + case CHAT -> player.chat(executable); + case CONSOLE -> scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + case MINI_MESSAGE -> + plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + case MINI_BROADCAST -> + plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); + case MESSAGE -> player.sendMessage(StringUtils.color(executable)); + case ACTION_BAR -> + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(StringUtils.color(executable))); + case LOG -> { final String[] logParts = executable.split(" ", 2); if (logParts.length == 0 || logParts[0].isBlank()) { @@ -152,18 +135,10 @@ public void run() { } plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); - break; - - case BROADCAST: - Bukkit.broadcastMessage(StringUtils.color(executable)); - break; - - case CLOSE: - Menu.closeMenu(plugin, player, true, true); - break; - - case OPEN_GUI_MENU: - case OPEN_MENU: + } + case BROADCAST -> Bukkit.broadcastMessage(StringUtils.color(executable)); + case CLOSE -> Menu.closeMenu(plugin, player, true, true, false); + case OPEN_GUI_MENU, OPEN_MENU -> { final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); final String[] executableParts = temporaryExecutable.split(" ", 2); @@ -193,9 +168,9 @@ public void run() { if (menuArgumentNames.isEmpty()) { if (passedArgumentValues != null && passedArgumentValues.length > 0) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" + DebugLevel.HIGHEST, + Level.WARNING, + "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" ); } @@ -221,9 +196,9 @@ public void run() { if (passedArgumentValues.length < menuArgumentNames.size()) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); break; } @@ -241,9 +216,9 @@ public void run() { if (passedArgumentValues.length <= index) { // This should never be the case! plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); break; } @@ -264,35 +239,24 @@ public void run() { } menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); - break; - - case CONNECT: - plugin.connect(player, executable); - break; - - case JSON_MESSAGE: - AdventureUtils.sendJson(plugin, player, executable); - break; - - case JSON_BROADCAST: - case BROADCAST_JSON: - plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); - break; - - case REFRESH: + } + case CONNECT -> plugin.connect(player, executable); + case JSON_MESSAGE -> AdventureUtils.sendJson(plugin, player, executable); + case JSON_BROADCAST, BROADCAST_JSON -> + plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + case REFRESH -> { if (holder.isEmpty()) { plugin.debug( - DebugLevel.MEDIUM, - Level.WARNING, - player.getName() + " does not have menu open! Nothing to refresh!" + DebugLevel.MEDIUM, + Level.WARNING, + player.getName() + " does not have menu open! Nothing to refresh!" ); break; } holder.get().refreshMenu(); - break; - - case TAKE_MONEY: + } + case TAKE_MONEY -> { if (plugin.getVault() == null || !plugin.getVault().hooked()) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); break; @@ -302,14 +266,13 @@ public void run() { plugin.getVault().takeMoney(player, Double.parseDouble(executable)); } catch (final NumberFormatException exception) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take money action: " + executable + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take money action: " + executable + ", is not a valid number!" ); } - break; - - case GIVE_MONEY: + } + case GIVE_MONEY -> { if (plugin.getVault() == null || !plugin.getVault().hooked()) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); break; @@ -319,19 +282,17 @@ public void run() { plugin.getVault().giveMoney(player, Double.parseDouble(executable)); } catch (final NumberFormatException exception) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give money action: " + executable + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give money action: " + executable + ", is not a valid number!" ); } - break; - - case TAKE_EXP: - case GIVE_EXP: + } + case TAKE_EXP, GIVE_EXP -> { final String lowerCaseExecutable = executable.toLowerCase(); try { - if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + if (Integer.parseInt(lowerCaseExecutable.replace("l", "")) <= 0) break; if (actionType == ActionType.TAKE_EXP) { ExpUtils.setExp(player, "-" + lowerCaseExecutable); @@ -339,56 +300,48 @@ public void run() { } ExpUtils.setExp(player, lowerCaseExecutable); - break; } catch (final NumberFormatException exception) { if (actionType == ActionType.TAKE_EXP) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take exp action: " + executable + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take exp action: " + executable + ", is not a valid number!" ); break; } plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give exp action: " + executable + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give exp action: " + executable + ", is not a valid number!" ); - break; } - - case GIVE_PERM: + } + case GIVE_PERM -> { if (plugin.getVault() == null || !plugin.getVault().hooked()) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot give permission: " + executable + "!"); + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot give permission: " + executable + "!"); break; } plugin.getVault().givePermission(player, executable); - break; - - case TAKE_PERM: + } + case TAKE_PERM -> { if (plugin.getVault() == null || !plugin.getVault().hooked()) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot take permission: " + executable + "!"); + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot take permission: " + executable + "!"); break; } plugin.getVault().takePermission(player, executable); - break; - - case BROADCAST_SOUND: - case BROADCAST_RAW_SOUND: - case BROADCAST_WORLD_SOUND: - case BROADCAST_WORLD_RAW_SOUND: - case PLAY_RAW_SOUND: - case PLAY_SOUND: + } + case BROADCAST_SOUND, BROADCAST_RAW_SOUND, BROADCAST_WORLD_SOUND, BROADCAST_WORLD_RAW_SOUND, PLAY_RAW_SOUND, + PLAY_SOUND -> { boolean isRaw = isRaw(actionType); Sound sound = null; @@ -402,8 +355,8 @@ public void run() { sound = SoundUtils.getSound(executable.toUpperCase()); } catch (final IllegalArgumentException exception) { plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception ); break; } @@ -417,8 +370,8 @@ public void run() { sound = SoundUtils.getSound(parts[0].toUpperCase()); } catch (final IllegalArgumentException exception) { plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception ); break; } @@ -429,101 +382,91 @@ public void run() { pitch = Float.parseFloat(parts[2]); } catch (final NumberFormatException exception) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Pitch given for sound action: " + parts[2] + ", is not a valid number!" ); plugin.printStacktrace( - "Pitch given for sound action: " + parts[2] + ", is not a valid number!", - exception + "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + exception ); } } - try { volume = Float.parseFloat(parts[1]); } catch (final NumberFormatException exception) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Volume given for sound action: " + parts[1] + ", is not a valid number!" + DebugLevel.HIGHEST, + Level.WARNING, + "Volume given for sound action: " + parts[1] + ", is not a valid number!" ); plugin.printStacktrace( - "Volume given for sound action: " + parts[1] + ", is not a valid number!", - exception + "Volume given for sound action: " + parts[1] + ", is not a valid number!", + exception ); } } switch (actionType) { - case BROADCAST_WORLD_RAW_SOUND: + case BROADCAST_WORLD_RAW_SOUND -> { for (final Player broadcastTarget : player.getWorld().getPlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); } - break; - - case BROADCAST_RAW_SOUND: + } + case BROADCAST_RAW_SOUND -> { for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); } - break; - - case PLAY_RAW_SOUND: - player.playSound(player.getLocation(), soundName, volume, pitch); - break; - - case BROADCAST_SOUND: + } + case PLAY_RAW_SOUND -> player.playSound(player.getLocation(), soundName, volume, pitch); + case BROADCAST_SOUND -> { if (sound == null) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" ); break; } for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); } - break; - - case BROADCAST_WORLD_SOUND: + } + case BROADCAST_WORLD_SOUND -> { if (sound == null) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" ); break; } for (final Player broadcastTarget : player.getWorld().getPlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); } - break; - - case PLAY_SOUND: + } + case PLAY_SOUND -> { if (sound == null) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" ); break; } player.playSound(player.getLocation(), sound, volume, pitch); - break; + } } - break; - - default: - break; + } + default -> { + } } } private boolean isRaw(ActionType actionType) { return actionType == ActionType.PLAY_RAW_SOUND || actionType == ActionType.BROADCAST_RAW_SOUND || actionType == ActionType.BROADCAST_WORLD_RAW_SOUND; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index 9aa91a97..f0870718 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -2,18 +2,19 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.command.subcommand.*; -import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.TabExecutor; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,7 +23,7 @@ import static net.kyori.adventure.text.Component.text; -public class DeluxeMenusCommand implements TabExecutor { +public class DeluxeMenusCommand implements BasicCommand { private static final TextReplacementConfig.Builder VERSION_REPLACER_BUILDER = TextReplacementConfig.builder().matchLiteral(""); private static final TextReplacementConfig.Builder AUTHORS_REPLACER_BUILDER = TextReplacementConfig.builder().matchLiteral(""); @@ -35,48 +36,47 @@ public DeluxeMenusCommand(final @NotNull DeluxeMenus plugin) { } public boolean register() { - final PluginCommand command = this.plugin.getCommand("deluxemenus"); - if (command == null) { - return false; - } - - command.setExecutor(this); registerSubCommands(); + plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> + event.registrar().register( + "deluxemenus", + "DeluxeMenus main command", + List.of("dm"), + this + ) + ); return true; } @Override - public boolean onCommand( - final @NotNull CommandSender sender, - final @NotNull Command command, - final @NotNull String label, - final @NotNull String[] args + public void execute( + final @NotNull CommandSourceStack stack, + final @NotNull String @NonNull [] args ) { + final CommandSender sender = stack.getSender(); final List arguments = Arrays.asList(args); - if (arguments.isEmpty()) { + if (arguments.isEmpty() || (arguments.size() == 1 && arguments.getFirst().isEmpty())) { plugin.sms(sender, Messages.PLUGIN_VERSION.message().replaceText(VERSION_REPLACER_BUILDER.replacement(plugin.getDescription().getVersion()).build()).replaceText(AUTHORS_REPLACER_BUILDER.replacement(plugin.getDescription().getAuthors().stream().map(author -> text(author, NamedTextColor.WHITE)).collect(Component.toComponent(text(", ", NamedTextColor.GRAY)))).build())); - return true; + return; } - final SubCommand subCommand = subCommands.get(arguments.get(0).toLowerCase()); + final SubCommand subCommand = subCommands.get(arguments.getFirst().toLowerCase()); if (subCommand != null) { subCommand.execute(sender, arguments.subList(1, arguments.size())); - return true; + return; } plugin.sms(sender, Messages.WRONG_USAGE); - return true; } @Override - public List onTabComplete( - final @NotNull CommandSender sender, - final @NotNull Command command, - final @NotNull String label, - final @NotNull String[] args + public @NotNull Collection suggest( + final @NotNull CommandSourceStack stack, + final @NotNull String @NonNull [] args ) { + final CommandSender sender = stack.getSender(); final List arguments = Arrays.asList(args); return subCommands.values() diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DumpCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DumpCommand.java index e59e0d05..8d0e3075 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DumpCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DumpCommand.java @@ -42,7 +42,7 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List menus = Menu.getAllMenus(); if (menus.isEmpty()) { plugin.sms(sender, Messages.MENUS_LOADED.message().replaceText(AMOUNT_REPLACER_BUILDER.replacement("There are no").build())); @@ -85,11 +85,11 @@ public void execute(@NotNull final CommandSender sender, @NotNull final List> paginatedMenus = getPaginatedMenus( @@ -373,6 +373,6 @@ private void addMenuToTreeRecursively(final Map tree, final List return null; } - return "/" + menu.options().commands().get(0); + return "/" + menu.options().commands().getFirst(); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/MetaCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/MetaCommand.java index 631aa206..65cbafc0 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/MetaCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/MetaCommand.java @@ -61,10 +61,10 @@ public void execute(@NotNull final CommandSender sender, @NotNull final List type = DataType.getSupportedTypeByName(typeName); if (type == null) { plugin.sms(sender, Messages.META_TYPE_UNSUPPORTED.message().replaceText(TYPE_REPLACER_BUILDER.replacement(typeName).build())); @@ -318,7 +318,7 @@ private void handleShowMeta(@NotNull final CommandSender sender, @NotNull final return; } - final String keyName = arguments.get(0); + final String keyName = arguments.getFirst(); final NamespacedKey namespacedKey = plugin.getPersistentMetaHandler().getKey(keyName); if (namespacedKey == null) { plugin.sms(sender, Messages.META_KEY_INVALID.message().replaceText(KEY_REPLACER_BUILDER.replacement(keyName).build())); @@ -366,25 +366,18 @@ private void handleSetMeta(@NotNull final CommandSender sender, @NotNull final P final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().setMetaValue(target, namespacedKey, type, parsedValue); switch (result) { - case SUCCESS: - plugin.sms(sender, Messages.META_VALUE_SET.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) - ); - return; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_VALUE_TYPE_MISMATCH.message() - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build())); - return; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_SET_COMMAND); + case SUCCESS -> plugin.sms(sender, Messages.META_VALUE_SET.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + ); + case NEW_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_VALUE_TYPE_MISMATCH.message() + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build())); + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_SET_COMMAND); } } @@ -394,26 +387,19 @@ private void handleRemoveMeta(@NotNull final CommandSender sender, @NotNull fina @NotNull final Map context) { final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().removeMetaValue(target, namespacedKey, type); switch (result) { - case SUCCESS: - plugin.sms(sender, Messages.META_VALUE_REMOVED.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) - ); - return; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); - return; - case VALUE_NOT_FOUND: - plugin.sms(sender, Messages.NO_META_VALUE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) - ); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_REMOVE_COMMAND); + case SUCCESS -> plugin.sms(sender, Messages.META_VALUE_REMOVED.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + ); + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); + case VALUE_NOT_FOUND -> plugin.sms(sender, Messages.NO_META_VALUE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + ); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_REMOVE_COMMAND); } } @@ -422,21 +408,18 @@ private void handleSwitchMeta(@NotNull final CommandSender sender, @NotNull fina final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().switchMetaValue(target, namespacedKey); switch (result) { - case SUCCESS: + case SUCCESS -> { final Object newValue = plugin.getPersistentMetaHandler().getMetaValue(target, namespacedKey, DataType.BOOLEAN); plugin.sms(sender, Messages.META_VALUE_SWITCHED.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) ); - return; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_SWITCH_COMMAND); + } + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_SWITCH_COMMAND); } } @@ -449,34 +432,29 @@ private void handleAddMeta(@NotNull final CommandSender sender, @NotNull final P if (!(parsedValue instanceof Number)) { plugin.sms(sender, Messages.META_ADD_TYPE_MISMATCH.message() - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(value)).build())); + .replaceText(VALUE_REPLACER_BUILDER.replacement(value).build())); return; } final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().addMetaValue(target, namespacedKey, type, (Number) parsedValue); switch (result) { - case SUCCESS: + case SUCCESS -> { final Object newValue = plugin.getPersistentMetaHandler().getMetaValue(target, namespacedKey, type); plugin.sms(sender, Messages.META_VALUE_ADDED.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) - .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) + .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) ); - return; - case INVALID_TYPE: - plugin.sms(sender, Messages.META_ADD_TYPE_MISMATCH.message() - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build())); - return; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_ADD_COMMAND); + } + case INVALID_TYPE -> plugin.sms(sender, Messages.META_ADD_TYPE_MISMATCH.message() + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build())); + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_ADD_COMMAND); } } @@ -489,34 +467,29 @@ private void handleSubtractMeta(@NotNull final CommandSender sender, @NotNull fi if (!(parsedValue instanceof Number)) { plugin.sms(sender, Messages.META_SUBTRACT_TYPE_MISMATCH.message() - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(value)).build())); + .replaceText(VALUE_REPLACER_BUILDER.replacement(value).build())); return; } final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().subtractMetaValue(target, namespacedKey, type, (Number) parsedValue); switch (result) { - case SUCCESS: + case SUCCESS -> { final Object newValue = plugin.getPersistentMetaHandler().getMetaValue(target, namespacedKey, type); plugin.sms(sender, Messages.META_VALUE_SUBTRACTED.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) - .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) - .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) - .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build()) + .replaceText(TYPE_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.TYPE_NAME, type.getComplexType().getSimpleName())).build()) + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build()) + .replaceText(NEW_VALUE_REPLACER_BUILDER.replacement(String.valueOf(newValue)).build()) + .replaceText(PLAYER_REPLACER_BUILDER.replacement(target.getName()).build()) ); - return; - case INVALID_TYPE: - plugin.sms(sender, Messages.META_SUBTRACT_TYPE_MISMATCH.message() - .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build())); - return; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() - .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_SUBTRACT_COMMAND); + } + case INVALID_TYPE -> plugin.sms(sender, Messages.META_SUBTRACT_TYPE_MISMATCH.message() + .replaceText(VALUE_REPLACER_BUILDER.replacement(String.valueOf(parsedValue)).build())); + case EXISTENT_VALUE_IS_DIFFERENT_TYPE -> plugin.sms(sender, Messages.META_EXISTENT_VALUE_WRONG_TYPE.message() + .replaceText(KEY_REPLACER_BUILDER.replacement(context.getOrDefault(ContextKeys.KEY_NAME, namespacedKey.toString())).build())); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_SUBTRACT_COMMAND); } } @@ -527,23 +500,12 @@ private void sendWrongUsageMessage(@NotNull final CommandSender sender, @Nullabl } switch (action) { - case SET: - plugin.sms(sender, Messages.WRONG_USAGE_META_SET_COMMAND); - return; - case REMOVE: - plugin.sms(sender, Messages.WRONG_USAGE_META_REMOVE_COMMAND); - return; - case ADD: - plugin.sms(sender, Messages.WRONG_USAGE_META_ADD_COMMAND); - return; - case SUBTRACT: - plugin.sms(sender, Messages.WRONG_USAGE_META_SUBTRACT_COMMAND); - return; - case SWITCH: - plugin.sms(sender, Messages.WRONG_USAGE_META_SWITCH_COMMAND); - return; - default: - plugin.sms(sender, Messages.WRONG_USAGE_META_COMMAND); + case SET -> plugin.sms(sender, Messages.WRONG_USAGE_META_SET_COMMAND); + case REMOVE -> plugin.sms(sender, Messages.WRONG_USAGE_META_REMOVE_COMMAND); + case ADD -> plugin.sms(sender, Messages.WRONG_USAGE_META_ADD_COMMAND); + case SUBTRACT -> plugin.sms(sender, Messages.WRONG_USAGE_META_SUBTRACT_COMMAND); + case SWITCH -> plugin.sms(sender, Messages.WRONG_USAGE_META_SWITCH_COMMAND); + default -> plugin.sms(sender, Messages.WRONG_USAGE_META_COMMAND); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/OpenCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/OpenCommand.java index 33cb874c..1e58ed91 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/OpenCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/OpenCommand.java @@ -127,10 +127,10 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List menu = Menu.getMenuByName(arguments.get(0)); + Optional menu = Menu.getMenuByName(arguments.getFirst()); if (menu.isEmpty()) { - plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.getFirst()).build())); return; } @@ -152,11 +152,11 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List menu = Menu.getMenuByName(arguments.get(0)); + Optional menu = Menu.getMenuByName(arguments.getFirst()); if (menu.isEmpty()) { - plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.getFirst()).build())); return; } menu.get().refreshForAll(); - if(arguments.size() < 2 || !arguments.get(1).equalsIgnoreCase("-s")) { + if (arguments.size() < 2 || !arguments.get(1).equalsIgnoreCase("-s")) { plugin.sms(sender, Messages.MENU_REFRESHED.message() .replaceText(MENU_REPLACER_BUILDER.replacement(menu.get().options().name()).build()) .replaceText(AMOUNT_REPLACER_BUILDER.replacement(String.valueOf(menu.get().activeViewers())).build()) @@ -74,11 +74,11 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List guiCloseCommands = new ArrayList<>(); + if (c.isList(pre + "gui_close_command")) { + guiCloseCommands.addAll(c.getStringList(pre + "gui_close_command")); + } else { + guiCloseCommands.add(c.getString(pre + "gui_close_command")); + } + builder.guiCloseCommands(guiCloseCommands); + } + + if (c.contains(pre + "gui_open_command")) { + final List guiOpenCommands = new ArrayList<>(); + if (c.isList(pre + "gui_open_command")) { + guiOpenCommands.addAll(c.getStringList(pre + "gui_open_command")); + } else { + guiOpenCommands.add(c.getString(pre + "gui_open_command")); + } + builder.guiOpenCommands(guiOpenCommands); + } + int size = 54; if (type == InventoryType.CHEST) { if (!c.contains(pre + "size")) { @@ -877,17 +895,16 @@ private RequirementList getRequirements(FileConfiguration c, String path) { boolean invert; switch (type) { - case HAS_ITEM: - case DOES_NOT_HAVE_ITEM: + case HAS_ITEM, DOES_NOT_HAVE_ITEM -> { ItemWrapper wrapper = new ItemWrapper(); if (c.contains(rPath + ".material")) { String materialName = c.getString(rPath + ".material"); try { if (!containsPlaceholders(materialName) && plugin.getItemHooks().values() - .stream() - .filter(x -> materialName.toLowerCase().startsWith(x.getPrefix())) - .findFirst() - .orElse(null) == null) + .stream() + .filter(x -> materialName.toLowerCase().startsWith(x.getPrefix())) + .findFirst() + .orElse(null) == null) Material.valueOf(materialName.toUpperCase()); } catch (Exception ex) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "has item requirement at path: " + rPath + " does not specify a valid Material name!"); @@ -928,11 +945,11 @@ private RequirementList getRequirements(FileConfiguration c, String path) { final ConfigurationSection modelDataComponent = c.getConfigurationSection(rPath + ".model_data_component"); if (modelDataComponent != null) { wrapper.setCustomModelDataComponent( - CustomModelDataComponent.builder() - .colors(modelDataComponent.getStringList("colors")) - .flags(modelDataComponent.getStringList("flags")) - .floats(modelDataComponent.getStringList("floats")) - .strings(modelDataComponent.getStringList("strings"))); + CustomModelDataComponent.builder() + .colors(modelDataComponent.getStringList("colors")) + .flags(modelDataComponent.getStringList("flags")) + .floats(modelDataComponent.getStringList("floats")) + .strings(modelDataComponent.getStringList("strings"))); } } @@ -962,92 +979,78 @@ private RequirementList getRequirements(FileConfiguration c, String path) { invert = type == RequirementType.DOES_NOT_HAVE_ITEM; req = new HasItemRequirement(plugin, wrapper, invert); - break; - case HAS_PERMISSION: - case DOES_NOT_HAVE_PERMISSION: + } + case HAS_PERMISSION, DOES_NOT_HAVE_PERMISSION -> { if (c.contains(rPath + ".permission")) { invert = type == RequirementType.DOES_NOT_HAVE_PERMISSION; req = new HasPermissionRequirement(c.getString(rPath + ".permission"), invert); } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Has Permission requirement at path: " + rPath + " does not contain a permission: entry"); } - break; - case HAS_PERMISSIONS: - case DOES_NOT_HAVE_PERMISSIONS: + } + case HAS_PERMISSIONS, DOES_NOT_HAVE_PERMISSIONS -> { if (c.contains(rPath + ".permissions")) { invert = type == RequirementType.DOES_NOT_HAVE_PERMISSIONS; int minimum = -1; if (c.contains(rPath + ".minimum") && (minimum = c.getInt(rPath + ".minimum")) < 1) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Has Permissions requirement at path: " + rPath + " has a minimum lower than 1. All permissions will be checked" + DebugLevel.HIGHEST, + Level.WARNING, + "Has Permissions requirement at path: " + rPath + " has a minimum lower than 1. All permissions will be checked" ); minimum = -1; } List permissions = c.getStringList(rPath + ".permissions"); if (permissions.isEmpty()) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Has Permissions requirement at path: " + rPath + " has no permissions to check. Ignoring..." + DebugLevel.HIGHEST, + Level.WARNING, + "Has Permissions requirement at path: " + rPath + " has no permissions to check. Ignoring..." ); break; } else if (minimum > permissions.size()) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Has Permissions requirement at path: " + rPath + " has a minimum higher than the amount of permissions. Using " + permissions.size() + " instead" + DebugLevel.HIGHEST, + Level.WARNING, + "Has Permissions requirement at path: " + rPath + " has a minimum higher than the amount of permissions. Using " + permissions.size() + " instead" ); minimum = permissions.size(); } req = new HasPermissionsRequirement(permissions, minimum, invert); } else { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Has Permissions requirement at path: " + rPath + " does not contain permissions: entry" + DebugLevel.HIGHEST, + Level.WARNING, + "Has Permissions requirement at path: " + rPath + " does not contain permissions: entry" ); } - break; - case JAVASCRIPT: + } + case JAVASCRIPT -> { if (c.contains(rPath + ".expression")) { req = new JavascriptRequirement(plugin, c.getString(rPath + ".expression")); } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Javascript requirement at path: " + rPath + " does not contain an expression: entry"); } - break; - case EQUAL_TO: - case NOT_EQUAL_TO: - case GREATER_THAN: - case GREATER_THAN_EQUAL_TO: - case LESS_THAN: - case LESS_THAN_EQUAL_TO: - case STRING_CONTAINS: - case STRING_EQUALS: - case STRING_EQUALS_IGNORECASE: - case STRING_DOES_NOT_CONTAIN: - case STRING_DOES_NOT_EQUAL: - case STRING_DOES_NOT_EQUAL_IGNORECASE: - case STRING_CONTAINS_IGNORECASE: - case STRING_DOES_NOT_CONTAIN_IGNORECASE: + } + case EQUAL_TO, NOT_EQUAL_TO, GREATER_THAN, GREATER_THAN_EQUAL_TO, LESS_THAN, LESS_THAN_EQUAL_TO, + STRING_CONTAINS, STRING_EQUALS, STRING_EQUALS_IGNORECASE, STRING_DOES_NOT_CONTAIN, + STRING_DOES_NOT_EQUAL, STRING_DOES_NOT_EQUAL_IGNORECASE, STRING_CONTAINS_IGNORECASE, + STRING_DOES_NOT_CONTAIN_IGNORECASE -> { if (c.contains(rPath + ".input") && c.contains(rPath + ".output")) { req = new InputResultRequirement(type, c.getString(rPath + ".input"), c.getString(rPath + ".output")); } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Requirement at path: " + rPath + " does not contain the input: and/or the output: entries"); } - break; - case HAS_MONEY: - case DOES_NOT_HAVE_MONEY: + } + case HAS_MONEY, DOES_NOT_HAVE_MONEY -> { if (c.contains(rPath + ".amount") || c.contains(rPath + ".placeholder")) { invert = type == RequirementType.DOES_NOT_HAVE_MONEY; req = new HasMoneyRequirement(plugin, c.getDouble(rPath + ".amount"), invert, c.getString(rPath + ".placeholder", null)); } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Has Money requirement at path: " + rPath + " does not contain an amount: entry"); } - break; - case HAS_EXP: - case DOES_NOT_HAVE_EXP: + } + case HAS_EXP, DOES_NOT_HAVE_EXP -> { if (c.contains(rPath + ".amount")) { if (!containsPlaceholders(c.getString(rPath + ".amount")) && !c.isInt(rPath + ".amount")) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Value at path: " + rPath + ".amount is not a placeholder or a number"); @@ -1058,9 +1061,8 @@ private RequirementList getRequirements(FileConfiguration c, String path) { } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Has Exp requirement at path: " + rPath + " does not contain an amount: entry"); } - break; - case REGEX_MATCHES: - case REGEX_DOES_NOT_MATCH: + } + case REGEX_MATCHES, REGEX_DOES_NOT_MATCH -> { if (c.contains(rPath + ".input") && c.contains(rPath + ".regex")) { Pattern p = Pattern.compile(c.getString(rPath + ".regex")); invert = type == RequirementType.REGEX_DOES_NOT_MATCH; @@ -1068,9 +1070,8 @@ private RequirementList getRequirements(FileConfiguration c, String path) { } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Regex requirement at path: " + rPath + " does not contain a input: or regex: entry"); } - break; - case IS_NEAR: - case IS_NOT_NEAR: + } + case IS_NEAR, IS_NOT_NEAR -> { if (c.contains(rPath + ".location") && c.contains(rPath + ".distance")) { invert = type == RequirementType.IS_NOT_NEAR; Location loc = LocationUtils.deserializeLocation(c.getString(rPath + ".location")); @@ -1081,9 +1082,8 @@ private RequirementList getRequirements(FileConfiguration c, String path) { } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Is Near requirement at path: " + rPath + " does not contain a location: or distance: entry"); } - break; - case HAS_META: - case DOES_NOT_HAVE_META: + } + case HAS_META, DOES_NOT_HAVE_META -> { if (!VersionHelper.IS_PDC_VERSION) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Has Meta requirement is not available for your server version!"); break; @@ -1095,8 +1095,8 @@ private RequirementList getRequirements(FileConfiguration c, String path) { } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Has Meta requirement at path: " + rPath + " does not contain the key:, meta_type: and/or value: entries!"); } - break; - case STRING_LENGTH: + } + case STRING_LENGTH -> { if (c.contains(rPath + ".input") && (c.contains(rPath + ".min") || c.contains(rPath + ".max"))) { int min = c.getInt(rPath + ".min", 0); Integer max = null; @@ -1107,16 +1107,16 @@ private RequirementList getRequirements(FileConfiguration c, String path) { } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "String length requirement at path: " + rPath + " does not contain an input: or one of (min: or max:)"); } - break; - case IS_OBJECT: + } + case IS_OBJECT -> { if (c.contains(rPath + ".input") && c.contains(rPath + ".object")) { req = new IsObjectRequirement(c.getString(rPath + ".input"), c.getString(rPath + ".object")); } else { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "String length requirement at path: " + rPath + " does not contain an input: or object:"); } - break; - default: - break; + } + default -> { + } } if (req != null) { @@ -1220,26 +1220,22 @@ private ClickHandler getClickHandler(FileConfiguration c, String configPath) { if (!actions.isEmpty()) { - handler = new ClickHandler() { - - @Override - public void onClick(@NotNull final MenuHolder holder) { - - for (ClickAction action : actions) { + handler = holder -> { - if (!action.checkChance(holder)) { - continue; - } + for (ClickAction action : actions) { - final ClickActionTask actionTask = new ClickActionTask(plugin, holder.getViewer().getUniqueId(), action.getType(), action.getExecutable(), holder.getTypedArgs(), holder.parsePlaceholdersInArguments(), holder.parsePlaceholdersAfterArguments()); + if (!action.checkChance(holder)) { + continue; + } - if (action.hasDelay()) { - actionTask.runTaskLater(plugin, action.getDelay(holder)); - continue; - } + final ClickActionTask actionTask = new ClickActionTask(plugin, holder.getViewer().getUniqueId(), action.getType(), action.getExecutable(), holder.getTypedArgs(), holder.parsePlaceholdersInArguments(), holder.parsePlaceholdersAfterArguments()); - actionTask.runTask(plugin); + if (action.hasDelay()) { + plugin.getScheduler().runTaskLater(holder.getViewer(), actionTask, action.getDelay(holder)); + continue; } + + plugin.getScheduler().runTask(holder.getViewer(), actionTask); } }; } diff --git a/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java b/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java index 5bbd1e15..9285ad92 100644 --- a/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java +++ b/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java @@ -2,6 +2,8 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.listener.Listener; +import com.extendedclip.deluxemenus.nbt.NbtProvider; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityPickupItemEvent; @@ -18,16 +20,19 @@ */ public class DupeFixer extends Listener { + private final TaskScheduler scheduler; private final MenuItemMarker marker; public DupeFixer(@NotNull final DeluxeMenus plugin, @NotNull final MenuItemMarker marker) { super(plugin); + this.scheduler = plugin.getScheduler(); this.marker = marker; } @EventHandler private void onPickup(@NotNull final EntityPickupItemEvent event) { - if (!marker.isMarked(event.getItem().getItemStack())) { + ItemStack stack = event.getItem().getItemStack(); + if (!marker.isMarked(stack) && isDupeProtectionFlagged(stack)) { return; } @@ -41,7 +46,8 @@ private void onPickup(@NotNull final EntityPickupItemEvent event) { @EventHandler private void onDrop(@NotNull final PlayerDropItemEvent event) { - if (!marker.isMarked(event.getItemDrop().getItemStack())) { + ItemStack stack = event.getItemDrop().getItemStack(); + if (!marker.isMarked(stack) && isDupeProtectionFlagged(stack)) { return; } @@ -55,21 +61,28 @@ private void onDrop(@NotNull final PlayerDropItemEvent event) { @EventHandler private void onLogin(@NotNull final PlayerJoinEvent event) { - plugin.getServer().getScheduler().runTaskLater( - plugin, - () -> { - for (final ItemStack itemStack : event.getPlayer().getInventory().getContents()) { - if (itemStack == null) continue; - if (!marker.isMarked(itemStack)) continue; + scheduler.runTaskLater(() -> { + for (final ItemStack itemStack : event.getPlayer().getInventory().getContents()) { + if (itemStack == null) continue; + if (!marker.isMarked(itemStack) && isDupeProtectionFlagged(itemStack)) continue; - plugin.debug( - DebugLevel.LOWEST, - Level.INFO, - "Player logged in with a DeluxeMenus item in their inventory. Removing it." - ); - event.getPlayer().getInventory().remove(itemStack); - }}, - 10L + plugin.debug( + DebugLevel.LOWEST, + Level.INFO, + "Player logged in with a DeluxeMenus item in their inventory. Removing it." + ); + event.getPlayer().getInventory().remove(itemStack); + } + }, 10L ); } -} \ No newline at end of file + + private boolean isDupeProtectionFlagged(ItemStack itemStack) { + if (NbtProvider.isAvailable()) { + String value = NbtProvider.getString(itemStack, "deluxemenus.item.dupeprotection"); + return !"true".equals(value); + } + + return true; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java b/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java index 655c4831..ec8ca03e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java +++ b/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java @@ -1,6 +1,5 @@ package com.extendedclip.deluxemenus.events; -import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java index 0178ec75..5aaf2128 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java @@ -5,7 +5,6 @@ import com.extendedclip.deluxemenus.utils.SkullUtils; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableBlocksHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableBlocksHook.java index 58ec1bf2..d92173b2 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableBlocksHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableBlocksHook.java @@ -15,7 +15,6 @@ public class ExecutableBlocksHook implements ItemHook, SimpleCache { private final Map cache = new ConcurrentHashMap<>(); - @SuppressWarnings("deprecation") @Override public ItemStack getItem(@NotNull String... arguments) { if (arguments.length == 0) { diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java index 9bdf1266..3054c1da 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; - import com.ssomar.score.api.executableitems.config.ExecutableItemInterface; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java index ac3760b0..bae2d184 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java @@ -4,7 +4,6 @@ import dev.lone.itemsadder.api.CustomStack; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java index d458c7bf..fad24825 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java @@ -2,15 +2,14 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.cache.SimpleCache; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; - import net.Indyuce.mmoitems.MMOItems; import net.Indyuce.mmoitems.api.Type; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -19,9 +18,11 @@ public class MMOItemsHook implements ItemHook, SimpleCache { private final Map cache = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; + private final TaskScheduler scheduler; public MMOItemsHook(final @NotNull DeluxeMenus plugin) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); } @Override @@ -47,7 +48,7 @@ public ItemStack getItem(@NotNull final String... arguments) { ItemStack mmoItem = null; try { - mmoItem = Bukkit.getScheduler().callSyncMethod(plugin, () -> { + mmoItem = scheduler.callSyncMethod(() -> { ItemStack item = MMOItems.plugin.getItem(itemType, splitArgs[1]); if (item == null) { diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java index aa880ba5..4b306fd9 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java @@ -6,7 +6,6 @@ import com.extendedclip.deluxemenus.utils.SkullUtils; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.ItemStack; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/SimpleItemGeneratorHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/SimpleItemGeneratorHook.java index 58e7eccb..8ec99f25 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/SimpleItemGeneratorHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/SimpleItemGeneratorHook.java @@ -15,7 +15,6 @@ public class SimpleItemGeneratorHook implements ItemHook, SimpleCache { - private final Map cache = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; @@ -34,9 +33,9 @@ public ItemStack getItem(@NotNull Player holder, @NotNull String... arguments) { return errorItem("Item arguments are absent."); } - final ItemStack item = cache.computeIfAbsent(arguments[0], id -> { - return SimpleItemGenerator.get().bakeItem(id, holder).orElse(null); - }); + final ItemStack item = cache.computeIfAbsent(arguments[0], id -> + SimpleItemGenerator.get().bakeItem(id, holder).orElse(null) + ); return item == null? errorItem("Item %s wasn't found by SimpleItemGenerator.", arguments[0]) : item.clone(); } diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index af69231b..830b1471 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -6,9 +6,10 @@ import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.menu.MenuItem; import com.extendedclip.deluxemenus.requirement.RequirementList; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.utils.DebugLevel; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -18,14 +19,17 @@ import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public class PlayerListener extends Listener { + private final TaskScheduler scheduler; private final Cache cache = CacheBuilder.newBuilder().expireAfterWrite(75, TimeUnit.MILLISECONDS).build(); // This is so dumb. Mojang fix your shit. @@ -33,9 +37,10 @@ public class PlayerListener extends Listener { public PlayerListener(@NotNull final DeluxeMenus plugin) { super(plugin); + this.scheduler = plugin.getScheduler(); } - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onCommandExecute(PlayerCommandPreprocessEvent event) { final String cmd = event.getMessage().substring(1); @@ -67,48 +72,74 @@ public void onLeave(PlayerQuitEvent event) { @EventHandler public void onOpen(InventoryOpenEvent event) { - if (!(event.getPlayer() instanceof Player)) { + if (!(event.getPlayer() instanceof Player player)) { return; } - final Player player = (Player) event.getPlayer(); - if (player.isSleeping()) { event.setCancelled(true); } if (Menu.isInMenu(player)) { - Menu.closeMenu(plugin, player, true); + Menu.closeMenu(plugin, player, false); } } @EventHandler public void onClose(InventoryCloseEvent event) { - - if (!(event.getPlayer() instanceof Player)) { + if (!(event.getPlayer() instanceof Player player)) { return; } - final Player player = (Player) event.getPlayer(); - if (Menu.isInMenu(player)) { Menu.closeMenu(plugin, player, false); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - Menu.cleanInventory(plugin, player); - player.updateInventory(); - }, 3L); } + + scheduler.runTaskLater(player, () -> { + Menu.cleanInventory(plugin, player); + + boolean removed = false; + + for (ItemStack itemStack : player.getInventory().getContents()) { + if (itemStack == null) continue; + if (!plugin.getMenuItemMarker().isMarked(itemStack)) continue; + if (!plugin.isDupeProtectionFlagged(itemStack)) continue; + + player.getInventory().remove(itemStack); + removed = true; + + plugin.debug( + DebugLevel.LOWEST, + Level.INFO, + "DeluxeMenus item found in main inventory on close. Removing it." + ); + } + + ItemStack offhand = player.getInventory().getItemInOffHand(); + if (plugin.getMenuItemMarker().isMarked(offhand) && plugin.isDupeProtectionFlagged(offhand)) { + player.getInventory().setItemInOffHand(null); + removed = true; + + plugin.debug( + DebugLevel.LOWEST, + Level.INFO, + "DeluxeMenus item found in offhand on close. Removing it." + ); + } + + if (removed) { + player.updateInventory(); + } + }, 1L); } @EventHandler(priority = EventPriority.LOW) public void onClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player)) { + if (!(event.getWhoClicked() instanceof Player player)) { return; } - final Player player = (Player) event.getWhoClicked(); - final Optional optionalHolder = Menu.getMenuHolder(player); if (optionalHolder.isEmpty()) { @@ -181,8 +212,7 @@ public void onClick(InventoryClickEvent event) { } if (event.getClick() == ClickType.MIDDLE) { - if (handleClick(player, holder, item.options().middleClickHandler(), item.options().middleClickRequirements())) { - } + handleClick(player, holder, item.options().middleClickHandler(), item.options().middleClickRequirements()); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 615bedf7..cbe3d37d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -1,19 +1,32 @@ package com.extendedclip.deluxemenus.menu; import com.extendedclip.deluxemenus.DeluxeMenus; -import com.extendedclip.deluxemenus.action.ClickHandler; -import com.extendedclip.deluxemenus.dupe.MenuItemMarker; +import com.extendedclip.deluxemenus.action.ActionType; +import com.extendedclip.deluxemenus.action.ClickAction; +import com.extendedclip.deluxemenus.action.ClickActionTask; +import com.extendedclip.deluxemenus.config.DeluxeMenusConfig; import com.extendedclip.deluxemenus.events.DeluxeMenusOpenMenuEvent; import com.extendedclip.deluxemenus.events.DeluxeMenusPreOpenMenuEvent; import com.extendedclip.deluxemenus.menu.command.RegistrableMenuCommand; import com.extendedclip.deluxemenus.menu.options.MenuOptions; import com.extendedclip.deluxemenus.requirement.RequirementList; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.StringUtils; - -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -25,11 +38,12 @@ public class Menu { - private static final Map menus = new HashMap<>(); - private static final Set menuHolders = new HashSet<>(); - private static final Map lastOpenedMenus = new HashMap<>(); + private static final Map menus = new ConcurrentHashMap<>(); + private static final Set menuHolders = ConcurrentHashMap.newKeySet(); + private static final Map lastOpenedMenus = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final MenuOptions options; private final Map> items; // menu path starting from the plugin directory @@ -44,6 +58,7 @@ public Menu( final @NotNull String path ) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.options = options; this.items = items; this.path = path; @@ -183,7 +198,7 @@ public static void cleanInventory(final @NotNull DeluxeMenus plugin, final @NotN player.updateInventory(); } - public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull Player player, final boolean close, final boolean executeCloseActions) { + public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull Player player, final boolean close, final boolean executeCloseActions, final boolean runCloseCommmands) { Optional optionalHolder = getMenuHolder(player); if (optionalHolder.isEmpty()) { return; @@ -198,14 +213,26 @@ public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull P holder.getMenu().map(Menu::options).map(MenuOptions::closeHandler).flatMap(h -> h).ifPresent(h -> h.onClick(holder)); } + menuHolders.remove(holder); + lastOpenedMenus.put(player.getUniqueId(), holder.getMenu().orElse(null)); + if (close) { - Bukkit.getScheduler().runTask(plugin, () -> { + final TaskScheduler scheduler = plugin.getScheduler(); + final Runnable closeInventory = () -> { player.closeInventory(); cleanInventory(plugin, player); - }); + }; + + if (scheduler.isEntityThread(player)) { + closeInventory.run(); + } else { + scheduler.runTask(player, closeInventory); + } + } + + if (runCloseCommmands) { + holder.getMenu().map(Menu::options).map(MenuOptions::guiCloseCommands).ifPresent(commands -> executeCommands(plugin, player, commands, holder)); } - menuHolders.remove(holder); - lastOpenedMenus.put(player.getUniqueId(), holder.getMenu().orElse(null)); } public static void closeMenuForShutdown(final @NotNull DeluxeMenus plugin, final @NotNull Player player) { @@ -216,7 +243,7 @@ public static void closeMenuForShutdown(final @NotNull DeluxeMenus plugin, final } public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull Player player, final boolean close) { - closeMenu(plugin, player, close, false); + closeMenu(plugin, player, close, true, true); } private boolean hasOpenBypassPerm(final @NotNull Player viewer) { @@ -274,11 +301,13 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + scheduler.runTaskAsynchronously(() -> { Set activeItems = new HashSet<>(); @@ -361,6 +390,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { - if(options.refresh()) { + holder.getMenu().map(Menu::options).map(MenuOptions::guiOpenCommands).ifPresent(commands -> executeCommands(plugin, viewer, commands, holder)); + + scheduler.runTask(viewer, () -> { + if (options.refresh()) { holder.startRefreshTask(); } @@ -395,17 +427,60 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + DeluxeMenusOpenMenuEvent openEvent = new DeluxeMenusOpenMenuEvent(viewer, holder); + Bukkit.getPluginManager().callEvent(openEvent); + }); + }); + } + + private static void executeCommands(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer, final @NotNull List commands, final @NotNull MenuHolder holder) { + for (String command : commands) { + ActionType type = ActionType.getByStart(command); + if (type == null) continue; + + command = command.replaceFirst(Pattern.quote(type.getIdentifier()), "").trim(); + + ClickAction action = new ClickAction(type, command); + + Matcher d = DeluxeMenusConfig.DELAY_MATCHER.matcher(command); + + if (d.find()) { + action.setDelay(d.group(1)); + command = command.replaceFirst(Pattern.quote(d.group()), ""); + } + + Matcher ch = DeluxeMenusConfig.CHANCE_MATCHER.matcher(command); + + if (ch.find()) { + action.setChance(ch.group(1)); + command = command.replaceFirst(Pattern.quote(ch.group()), ""); + } + + action.setExecutable(command); + + final ClickActionTask actionTask = new ClickActionTask( + plugin, + viewer.getUniqueId(), + action.getType(), + command, + holder.getTypedArgs(), + true, + true + ); + + if (action.hasDelay()) { + plugin.getScheduler().runTaskLater(viewer, actionTask, action.getDelay(holder)); + } else { + plugin.getScheduler().runTask(viewer, actionTask); + } } - }); - - Bukkit.getScheduler().runTask(plugin, () -> { - DeluxeMenusOpenMenuEvent openEvent = new DeluxeMenusOpenMenuEvent(viewer, holder); - Bukkit.getPluginManager().callEvent(openEvent); - }); - }); - } + } public void refreshForAll() { menuHolders.stream().filter(menuHolder -> menuHolder.getMenuName().equalsIgnoreCase(options.name())).forEach(MenuHolder::refreshMenu); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index b101a365..34afac38 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -2,20 +2,16 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; import com.extendedclip.deluxemenus.utils.StringUtils; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -24,27 +20,32 @@ public class MenuHolder implements InventoryHolder { private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final Player viewer; private Player placeholderPlayer; private String menuName; private Set activeItems; - private BukkitTask updateTask = null; - private BukkitTask refreshTask = null; + private MyScheduledTask updateTask = null; + private MyScheduledTask refreshTask = null; private Inventory inventory; - private boolean updating; + + private volatile boolean updating; + private boolean parsePlaceholdersInArguments; private boolean parsePlaceholdersAfterArguments; private Map typedArgs; public MenuHolder(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.viewer = viewer; } public MenuHolder(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer, final @NotNull String menuName, final @NotNull Set<@NotNull MenuItem> activeItems, final @NotNull Inventory inventory) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.viewer = viewer; this.menuName = menuName; this.activeItems = activeItems; @@ -55,7 +56,7 @@ public String getViewerName() { return viewer.getName(); } - public BukkitTask getUpdateTask() { + public MyScheduledTask getUpdateTask() { return updateTask; } @@ -104,7 +105,7 @@ public Optional getMenu() { } public @NotNull String setPlaceholders(final @NotNull String string) { - final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.getViewer(); + final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.viewer; if (player == null) { return string; } @@ -113,7 +114,7 @@ public Optional getMenu() { } public @NotNull String setArguments(final @NotNull String string) { - final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.getViewer(); + final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.viewer; return StringUtils.replaceArguments( string, @@ -136,82 +137,89 @@ public void refreshMenu() { return; } - setUpdating(true); - - Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { + scheduler.runTaskAsynchronously(() -> { final Set active = new HashSet<>(); + final Set slotsToClear = new HashSet<>(); for (int i = 0; i < getInventory().getSize(); i++) { TreeMap e = menu.getMenuItems().get(i); if (e == null) { - getInventory().setItem(i, null); + slotsToClear.add(i); continue; } - boolean m = false; + boolean matched = false; for (MenuItem item : e.values()) { if (item.options().viewRequirements().isPresent()) { if (item.options().viewRequirements().get().evaluate(this)) { - m = true; + matched = true; active.add(item); break; } } else { - m = true; + matched = true; active.add(item); break; } } - if (!m) { - getInventory().setItem(i, null); + if (!matched) { + slotsToClear.add(i); } } if (active.isEmpty()) { - Menu.closeMenu(plugin, getViewer(), true); + scheduler.runTask(viewer, () -> Menu.closeMenu(plugin, viewer, true)); + return; } - Bukkit.getScheduler().runTask(plugin, () -> { + scheduler.runTask(viewer, () -> { + setUpdating(true); - boolean update = false; + try { + for (int slot : slotsToClear) { + getInventory().setItem(slot, null); + } - for (MenuItem item : active) { + boolean update = false; - ItemStack iStack = item.getItemStack(this); + for (MenuItem item : active) { - if (iStack == null) { - continue; - } + ItemStack iStack = item.getItemStack(this); - iStack = plugin.getMenuItemMarker().mark(iStack); + if (iStack == null) { + continue; + } - int slot = item.options().slot(); + iStack = plugin.getMenuItemMarker().mark(iStack); - if (slot >= menu.options().size()) { - continue; - } + int slot = item.options().slot(); - if (item.options().updatePlaceholders()) { - update = true; - } + if (slot >= menu.options().size()) { + continue; + } - getInventory().setItem(item.options().slot(), iStack); - } + if (item.options().updatePlaceholders()) { + update = true; + } - setActiveItems(active); + getInventory().setItem(item.options().slot(), iStack); + } - if (update && updateTask == null) { - startUpdatePlaceholdersTask(); - } else if(!update && updateTask != null) { - stopPlaceholderUpdate(); - } + setActiveItems(active); - setUpdating(false); + if (update && updateTask == null) { + startUpdatePlaceholdersTask(); + } else if (!update && updateTask != null) { + stopPlaceholderUpdate(); + } + } finally { + setUpdating(false); + } }); }); } @@ -227,7 +235,7 @@ public void stopPlaceholderUpdate() { } public void stopRefreshTask() { - if(refreshTask != null) { + if (refreshTask != null) { try { refreshTask.cancel(); } catch (Exception ignored) { @@ -237,20 +245,21 @@ public void stopRefreshTask() { } public void startRefreshTask() { - if(refreshTask != null) { + if (refreshTask != null) { stopRefreshTask(); } - refreshTask = new BukkitRunnable() { - @Override - public void run() { - refreshMenu(); - } - }.runTaskTimerAsynchronously(plugin, 20L, - 20L * Menu.getMenuByName(menuName) - .map(Menu::options) - .map(MenuOptions::refreshInterval) - .orElse(10)); + long initialDelay = 20L; + long period = 20L * Menu.getMenuByName(menuName) + .map(Menu::options) + .map(MenuOptions::refreshInterval) + .orElse(10); + + refreshTask = scheduler.runTaskTimerAsynchronously( + this::refreshMenu, + initialDelay, + period + ); } public void startUpdatePlaceholdersTask() { @@ -259,69 +268,70 @@ public void startUpdatePlaceholdersTask() { stopPlaceholderUpdate(); } - updateTask = new BukkitRunnable() { + long initialDelay = 20L; + long period = 20L * Menu.getMenuByName(menuName) + .map(Menu::options) + .map(MenuOptions::updateInterval) + .orElse(10); - @Override - public void run() { + updateTask = scheduler.runTaskTimer( + viewer, + () -> { - if (updating) { - return; - } - - Set items = getActiveItems(); + if (updating) { + return; + } - if (items == null) { - return; - } + Set items = getActiveItems(); - for (MenuItem item : items) { + if (items == null) { + return; + } - if (item.options().updatePlaceholders()) { + for (MenuItem item : items) { - ItemStack i = inventory.getItem(item.options().slot()); + if (item.options().updatePlaceholders()) { - if (i == null) { - continue; - } + ItemStack i = inventory.getItem(item.options().slot()); - int amt = i.getAmount(); + if (i == null) { + continue; + } - if (item.options().dynamicAmount().isPresent()) { - try { - amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get())); - if (amt <= 0) { - amt = 1; + int amt = i.getAmount(); + + if (item.options().dynamicAmount().isPresent()) { + try { + amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get())); + if (amt <= 0) { + amt = 1; + } + } catch (Exception exception) { + plugin.printStacktrace( + "Something went wrong while updating item in slot " + item.options().slot() + + ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get()), + exception + ); } - } catch (Exception exception) { - plugin.printStacktrace( - "Something went wrong while updating item in slot " + item.options().slot() + - ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get()), - exception - ); } - } - ItemMeta meta = i.getItemMeta(); + ItemMeta meta = i.getItemMeta(); - if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { - meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); - } + if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { + meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); + } - if (item.options().loreHasPlaceholders()) { - meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); - } + if (item.options().loreHasPlaceholders()) { + meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); + } - i.setItemMeta(meta); - i.setAmount(amt); + i.setItemMeta(meta); + i.setAmount(amt); + } } - } - } - }.runTaskTimerAsynchronously(plugin, 20L, - 20L * Menu.getMenuByName(menuName) - .map(Menu::options) - .map(MenuOptions::updateInterval) - .orElse(10)); + }, initialDelay, period + ); } public boolean isUpdating() { diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java index 79fbd8eb..a896cdb8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java @@ -2,10 +2,10 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.hooks.ItemHook; +import com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent; import com.extendedclip.deluxemenus.menu.options.HeadType; import com.extendedclip.deluxemenus.menu.options.LoreAppendMode; import com.extendedclip.deluxemenus.menu.options.MenuItemOptions; -import com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent; import com.extendedclip.deluxemenus.nbt.NbtProvider; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.ItemUtils; @@ -15,8 +15,8 @@ import org.bukkit.Color; import org.bukkit.FireworkEffect; import org.bukkit.Material; -import org.bukkit.Registry; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.block.Banner; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Light; @@ -45,14 +45,14 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.Base64; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Objects; +import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; @@ -81,14 +81,11 @@ public static ItemStack base64ToItemStack(String data) { return (ItemStack) object; } return null; - } catch (IllegalArgumentException e) { - return null; - } catch (IOException e) { - return null; - } catch (ClassNotFoundException e) { + } catch (IllegalArgumentException | ClassNotFoundException | IOException e) { return null; } } + public ItemStack getItemStack(@NotNull final MenuHolder holder) { final Player viewer = holder.getViewer(); @@ -112,7 +109,6 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } } - if (ItemUtils.isPlayerItem(lowercaseStringMaterial)) { final ItemStack playerItem = INVENTORY_ITEM_ACCESSORS.get(lowercaseStringMaterial).apply(viewer.getInventory()); @@ -282,20 +278,18 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { LoreAppendMode mode = this.options.loreAppendMode().orElse(LoreAppendMode.OVERRIDE); if (!this.options.hasLore() && this.options.loreAppendMode().isEmpty()) mode = LoreAppendMode.IGNORE; switch (mode) { - case IGNORE: // DM lore is not added at all - lore.addAll(itemLore); - break; - case TOP: // DM lore is added at the top + case IGNORE -> // DM lore is not added at all + lore.addAll(itemLore); + case TOP -> { lore.addAll(getMenuItemLore(holder, this.options.lore())); lore.addAll(itemLore); - break; - case BOTTOM: // DM lore is bottom at the bottom + } + case BOTTOM -> { lore.addAll(itemLore); lore.addAll(getMenuItemLore(holder, this.options.lore())); - break; - case OVERRIDE: // Lore from DM overrides the lore from the item - lore.addAll(getMenuItemLore(holder, this.options.lore())); - break; + } + case OVERRIDE -> // Lore from DM overrides the lore from the item + lore.addAll(getMenuItemLore(holder, this.options.lore())); } itemMeta.setLore(lore); @@ -373,69 +367,65 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { Level.WARNING, "Trim pattern is not set for item with trim material " + trimMaterialName.get() ); - } else if (trimPatternName.isPresent()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Trim material is not set for item with trim pattern " + trimPatternName.get() - ); - } + } else trimPatternName.ifPresent(s -> plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Trim material is not set for item with trim pattern " + s + )); } - if (itemMeta instanceof LeatherArmorMeta && this.options.rgb().isPresent()) { - final LeatherArmorMeta leatherArmorMeta = (LeatherArmorMeta) itemMeta; + switch (itemMeta) { + case LeatherArmorMeta leatherArmorMeta when this.options.rgb().isPresent() -> { - final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); - if (color != null) { - leatherArmorMeta.setColor(color); - } else { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Invalid rgb colors found for leather armor: " + this.options.rgb().get() - ); - } + final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); + if (color != null) { + leatherArmorMeta.setColor(color); + } else { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Invalid rgb colors found for leather armor: " + this.options.rgb().get() + ); + } - itemStack.setItemMeta(leatherArmorMeta); - } else if (itemMeta instanceof FireworkEffectMeta && this.options.rgb().isPresent()) { - final FireworkEffectMeta fireworkEffectMeta = (FireworkEffectMeta) itemMeta; - final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); - if (color != null) { - fireworkEffectMeta.setEffect(FireworkEffect.builder().withColor(color).build()); - } else { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Invalid RGB color found for firework or firework star: " + this.options.rgb().get() - ); + itemStack.setItemMeta(leatherArmorMeta); } - itemStack.setItemMeta(fireworkEffectMeta); - } else if (itemMeta instanceof EnchantmentStorageMeta && !this.options.enchantments().isEmpty()) { - final EnchantmentStorageMeta enchantmentStorageMeta = (EnchantmentStorageMeta) itemMeta; - for (final Map.Entry entry : this.options.enchantments().entrySet()) { - final boolean result = enchantmentStorageMeta.addStoredEnchant(entry.getKey(), entry.getValue(), true); - if (!result) { + case FireworkEffectMeta fireworkEffectMeta when this.options.rgb().isPresent() -> { + final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); + if (color != null) { + fireworkEffectMeta.setEffect(FireworkEffect.builder().withColor(color).build()); + } else { plugin.debug( - DebugLevel.HIGHEST, - Level.INFO, - "Failed to add enchantment " + entry.getKey().getName() + " to item " + itemStack.getType() + DebugLevel.HIGHEST, + Level.WARNING, + "Invalid RGB color found for firework or firework star: " + this.options.rgb().get() ); } + itemStack.setItemMeta(fireworkEffectMeta); } - itemStack.setItemMeta(enchantmentStorageMeta); - } else { - itemStack.setItemMeta(itemMeta); + case EnchantmentStorageMeta enchantmentStorageMeta when !this.options.enchantments().isEmpty() -> { + for (final Map.Entry entry : this.options.enchantments().entrySet()) { + final boolean result = enchantmentStorageMeta.addStoredEnchant(entry.getKey(), entry.getValue(), true); + if (!result) { + plugin.debug( + DebugLevel.HIGHEST, + Level.INFO, + "Failed to add enchantment " + entry.getKey().getName() + " to item " + itemStack.getType() + ); + } + } + itemStack.setItemMeta(enchantmentStorageMeta); + } + default -> itemStack.setItemMeta(itemMeta); } if (!(itemMeta instanceof EnchantmentStorageMeta) && !this.options.enchantments().isEmpty()) { this.options.enchantments().forEach((enchantment, level) -> itemMeta.addEnchant(enchantment, level, true)); } - if (this.options.lightLevel().isPresent() && itemMeta instanceof BlockDataMeta) { - final BlockDataMeta blockDataMeta = (BlockDataMeta) itemMeta; + if (this.options.lightLevel().isPresent() && itemMeta instanceof BlockDataMeta blockDataMeta) { final BlockData blockData = blockDataMeta.getBlockData(itemStack.getType()); - if (blockData instanceof Light) { - final Light light = (Light) blockData; + if (blockData instanceof Light light) { final String parsedLightLevel = holder.setPlaceholdersAndArguments(this.options.lightLevel().get()); try { final int lightLevel = Math.min(Integer.parseInt(parsedLightLevel), light.getMaximumLevel()); @@ -541,6 +531,10 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { itemStack = NbtProvider.setInt(itemStack, parts[0], Integer.parseInt(parts[1])); } } + + if (NbtProvider.isAvailable()) { + itemStack = NbtProvider.setString(itemStack, "deluxemenus.item.dupeprotection", "true"); + } } return itemStack; diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java b/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java index 286877dd..06feefd4 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java @@ -12,6 +12,7 @@ import org.bukkit.command.SimpleCommandMap; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.lang.reflect.Field; import java.util.Arrays; @@ -33,7 +34,7 @@ public class RegistrableMenuCommand extends Command { public RegistrableMenuCommand(final @NotNull DeluxeMenus plugin, final @NotNull Menu menu) { - super(menu.options().commands().isEmpty() ? menu.options().name() : menu.options().commands().get(0)); + super(menu.options().commands().isEmpty() ? menu.options().name() : menu.options().commands().getFirst()); this.plugin = plugin; this.menu = menu; @@ -43,12 +44,12 @@ public RegistrableMenuCommand(final @NotNull DeluxeMenus plugin, } @Override - public boolean execute(final @NotNull CommandSender sender, final @NotNull String commandLabel, final @NotNull String[] typedArgs) { + public boolean execute(final @NotNull CommandSender sender, final @NotNull String commandLabel, final @NotNull String @NonNull [] typedArgs) { if (this.unregistered) { throw new IllegalStateException("This command was unregistered!"); } - if (!(sender instanceof Player)) { + if (!(sender instanceof Player player)) { Msg.msg(sender, "Menus can only be opened by players!"); return true; } @@ -60,7 +61,7 @@ public boolean execute(final @NotNull CommandSender sender, final @NotNull Strin if (typedArgs.length < menu.options().arguments().size()) { if (menu.options().argumentsUsageMessage().isPresent()) { String usageMessage = menu.options().argumentsUsageMessage().get(); - Msg.msg(sender, StringUtils.replacePlaceholders(usageMessage, (Player) sender)); + Msg.msg(sender, StringUtils.replacePlaceholders(usageMessage, player)); } return true; } @@ -79,7 +80,6 @@ public boolean execute(final @NotNull CommandSender sender, final @NotNull Strin } } - Player player = (Player) sender; plugin.debug(DebugLevel.LOWEST, Level.INFO, "opening menu: " + menu.options().name()); menu.openMenu(player, argMap, null); return true; @@ -89,26 +89,9 @@ public void register() { if (registered) { throw new IllegalStateException("This command was already registered!"); } - if (registered) { - throw new IllegalStateException("This command was already registered!"); - } - registered = true; registered = true; - if (commandMap == null) { - try { - final Field f = Bukkit.getServer().getClass().getDeclaredField("commandMap"); - f.setAccessible(true); - commandMap = (CommandMap) f.get(Bukkit.getServer()); - } catch (final @NotNull Exception exception) { - plugin.printStacktrace( - "Something went wrong while trying to register command: " + this.getName(), - exception - ); - return; - } - } if (commandMap == null) { try { final Field f = Bukkit.getServer().getClass().getDeclaredField("commandMap"); @@ -164,6 +147,7 @@ public void unregister() { knownCommands = SimpleCommandMap.class.getDeclaredField("knownCommands"); knownCommands.setAccessible(true); + //noinspection unchecked final Map knownCommandsMap = (Map) knownCommands.get(cMap.get(Bukkit.getServer())); // We need to remove every single alias because CommandMap#register() adds them all to the map. diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java b/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java index c5c4df0a..2e4e2f90 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java @@ -23,6 +23,8 @@ public class MenuOptions { private final boolean enableBypassPerm; private final List commands; + private final List guiCloseCommands; + private final List guiOpenCommands; private final boolean registerCommands; private final List arguments; private final List argumentRequirements; @@ -45,6 +47,8 @@ private MenuOptions(final @NotNull MenuOptionsBuilder builder) { this.enableBypassPerm = builder.enableBypassPerm; this.commands = builder.commands; + this.guiCloseCommands = builder.guiCloseCommands; + this.guiOpenCommands = builder.guiOpenCommands; this.registerCommands = builder.registerCommands; this.arguments = builder.arguments; this.argumentRequirements = builder.argumentRequirements; @@ -103,6 +107,14 @@ public boolean enableBypassPerm() { return this.commands; } + public @Nullable List guiCloseCommands() { + return this.guiCloseCommands; + } + + public @Nullable List guiOpenCommands() { + return this.guiOpenCommands; + } + public boolean registerCommands() { return this.registerCommands; } @@ -142,6 +154,8 @@ public boolean registerCommands() { .parsePlaceholdersAfterArguments(this.parsePlaceholdersAfterArguments) .enableBypassPerm(this.enableBypassPerm) .commands(this.commands) + .guiCloseCommands(this.guiCloseCommands) + .guiOpenCommands(this.guiOpenCommands) .registerCommands(this.registerCommands) .arguments(this.arguments) .argumentRequirements(this.argumentRequirements) @@ -165,6 +179,8 @@ public static class MenuOptionsBuilder { private boolean enableBypassPerm = false; private List commands = List.of(); + private List guiCloseCommands; + private List guiOpenCommands; private boolean registerCommands = false; private List arguments = List.of(); private List argumentRequirements = List.of(); @@ -234,6 +250,16 @@ public MenuOptionsBuilder commands(final @NotNull List<@NotNull String> commands return this; } + public MenuOptionsBuilder guiCloseCommands(final @Nullable List guiCloseCommands) { + this.guiCloseCommands = guiCloseCommands; + return this; + } + + public MenuOptionsBuilder guiOpenCommands(final @Nullable List guiOpenCommands) { + this.guiOpenCommands = guiOpenCommands; + return this; + } + public MenuOptionsBuilder registerCommands(final boolean registerCommands) { this.registerCommands = registerCommands; return this; diff --git a/src/main/java/com/extendedclip/deluxemenus/persistentmeta/PersistentMetaHandler.java b/src/main/java/com/extendedclip/deluxemenus/persistentmeta/PersistentMetaHandler.java index bab7d857..f6c077ee 100644 --- a/src/main/java/com/extendedclip/deluxemenus/persistentmeta/PersistentMetaHandler.java +++ b/src/main/java/com/extendedclip/deluxemenus/persistentmeta/PersistentMetaHandler.java @@ -120,9 +120,9 @@ public Map getMetaValues( return player.getPersistentDataContainer().getKeys().stream() .filter(key -> player.getPersistentDataContainer().has(key, type.getPDType())) .map(key -> Pair.of(key.toString(), player.getPersistentDataContainer().get(key, type.getPDType()))) - .filter(entry -> entry.getValue() != null) - .filter(entry -> type.isSupported(entry.getValue())) - .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + .filter(entry -> entry.value() != null) + .filter(entry -> type.isSupported(entry.value())) + .collect(Collectors.toMap(Pair::key, Pair::value)); } /** @@ -223,7 +223,7 @@ public Map getMetaValues( return OperationResult.EXISTENT_VALUE_IS_DIFFERENT_TYPE; } - player.getPersistentDataContainer().set(key, DataType.BOOLEAN.getPDType(), currentValue.equalsIgnoreCase("true") ? "false" : "true"); + player.getPersistentDataContainer().set(key, DataType.BOOLEAN.getPDType(), Boolean.toString(!currentValue.equalsIgnoreCase("true"))); return OperationResult.SUCCESS; } @@ -352,30 +352,32 @@ public Map getMetaValues( final Object parsedValue = parseValueByType(type, args.length >= 4 ? args[3] : null); - switch (action) { - case SET: + return switch (action) { + case SET -> { if (parsedValue == null) { - return OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; + yield OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; } - return setMetaValue(player, key, type, parsedValue); - case REMOVE: - return removeMetaValue(player, key, type); - case ADD: + yield setMetaValue(player, key, type, parsedValue); + } + case REMOVE -> removeMetaValue(player, key, type); + case ADD -> { if (!(parsedValue instanceof Number)) { - return OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; + yield OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; } - return addMetaValue(player, key, type, (Number) parsedValue); - case SUBTRACT: + yield addMetaValue(player, key, type, (Number) parsedValue); + } + case SUBTRACT -> { if (!(parsedValue instanceof Number)) { - return OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; + yield OperationResult.NEW_VALUE_IS_DIFFERENT_TYPE; } - return subtractMetaValue(player, key, type, (Number) parsedValue); - } + yield subtractMetaValue(player, key, type, (Number) parsedValue); + } + default -> OperationResult.INVALID_SYNTAX; + }; - return OperationResult.INVALID_SYNTAX; } /** @@ -424,7 +426,6 @@ public Map getMetaValues( * @param key The string to parse. * @return The {@link NamespacedKey} or null if the key could not be parsed. */ - @SuppressWarnings("UnstableApiUsage") public @Nullable NamespacedKey getKey(@NotNull final String key) { final NamespacedKey namespacedKey; diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/Expansion.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/Expansion.java index 6a1a6ea0..ac467470 100644 --- a/src/main/java/com/extendedclip/deluxemenus/placeholder/Expansion.java +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/Expansion.java @@ -70,13 +70,13 @@ public boolean persist() { final String parsedInputLower = parsedInput.toLowerCase(); switch (parsedInputLower) { - case "is_in_menu": { + case "is_in_menu" -> { return getBooleanAsString(Menu.getMenuHolder(onlinePlayer).isPresent()); } - case "opened_menu": { + case "opened_menu" -> { return Menu.getOpenMenu(onlinePlayer).map(Menu::options).map(MenuOptions::name).orElse(""); } - case "last_menu": { + case "last_menu" -> { return Menu.getLastMenu(onlinePlayer).map(Menu::options).map(MenuOptions::name).orElse(""); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java index 9c1a1442..8a33e741 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java @@ -187,7 +187,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus .map(holder::setPlaceholdersAndArguments) .map(StringUtils::parseRGBColor) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); for (Color color : colors) { if (!modelData.getColors().contains(color)) { @@ -201,7 +201,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus .stream() .map(holder::setPlaceholdersAndArguments) .map(Boolean::parseBoolean) - .collect(Collectors.toList()); + .toList(); for (Boolean flag : flags) { if (!modelData.getFlags().contains(flag)) { @@ -215,7 +215,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus .stream() .map(holder::setPlaceholdersAndArguments) .map(Float::parseFloat) - .collect(Collectors.toList()); + .toList(); for (Float floatValue : floats) { if (!modelData.getFloats().contains(floatValue)) { @@ -228,7 +228,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus final List strings = wrapper.strings() .stream() .map(holder::setPlaceholdersAndArguments) - .collect(Collectors.toList()); + .toList(); for (String string : strings) { if (!modelData.getStrings().contains(string)) { diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java index 93932422..8b11ce83 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java @@ -21,24 +21,32 @@ public boolean evaluate(MenuHolder holder) { String parsedResult = holder.setPlaceholdersAndArguments(this.result); switch (type) { - case STRING_CONTAINS: - return parsedInput.contains(parsedResult); - case STRING_EQUALS: - return parsedInput.equals(parsedResult); - case STRING_EQUALS_IGNORECASE: - return parsedInput.equalsIgnoreCase(parsedResult); - case STRING_DOES_NOT_CONTAIN: - return !parsedInput.contains(parsedResult); - case STRING_DOES_NOT_EQUAL: - return !parsedInput.equals(parsedResult); - case STRING_DOES_NOT_EQUAL_IGNORECASE: - return !parsedInput.equalsIgnoreCase(parsedResult); - case STRING_CONTAINS_IGNORECASE: - return parsedInput.toLowerCase().contains(parsedResult.toLowerCase()); - case STRING_DOES_NOT_CONTAIN_IGNORECASE: - return !parsedInput.toLowerCase().contains(parsedResult.toLowerCase()); - default: - break; + case STRING_CONTAINS -> { + return parsedInput.contains(parsedResult); + } + case STRING_EQUALS -> { + return parsedInput.equals(parsedResult); + } + case STRING_EQUALS_IGNORECASE -> { + return parsedInput.equalsIgnoreCase(parsedResult); + } + case STRING_DOES_NOT_CONTAIN -> { + return !parsedInput.contains(parsedResult); + } + case STRING_DOES_NOT_EQUAL -> { + return !parsedInput.equals(parsedResult); + } + case STRING_DOES_NOT_EQUAL_IGNORECASE -> { + return !parsedInput.equalsIgnoreCase(parsedResult); + } + case STRING_CONTAINS_IGNORECASE -> { + return parsedInput.toLowerCase().contains(parsedResult.toLowerCase()); + } + case STRING_DOES_NOT_CONTAIN_IGNORECASE -> { + return !parsedInput.toLowerCase().contains(parsedResult.toLowerCase()); + } + default -> { + } } double in; @@ -64,23 +72,14 @@ public boolean evaluate(MenuHolder holder) { return false; } - switch (type) { - case GREATER_THAN: - return in > res; - case GREATER_THAN_EQUAL_TO: - return in >= res; - case EQUAL_TO: - return in == res; - case NOT_EQUAL_TO: - return in != res; - case LESS_THAN_EQUAL_TO: - return in <= res; - case LESS_THAN: - return in < res; - default: - break; - } - return false; + return switch (type) { + case GREATER_THAN -> in > res; + case GREATER_THAN_EQUAL_TO -> in >= res; + case EQUAL_TO -> in == res; + case NOT_EQUAL_TO -> in != res; + case LESS_THAN_EQUAL_TO -> in <= res; + case LESS_THAN -> in < res; + default -> false; + }; } } - diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java index d207aba7..e5b575a5 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java @@ -24,6 +24,6 @@ public boolean evaluate(MenuHolder holder) { if (holder.getViewer().getWorld().getName().equals(location.getWorld().getName())) { withinRange = holder.getViewer().getLocation().distance(location) < distance; } - return invert ? !withinRange : withinRange; + return invert != withinRange; } } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java index 467908fa..f9a1b79a 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java @@ -24,27 +24,32 @@ public boolean evaluate(MenuHolder holder) { String toCheck = holder.setPlaceholdersAndArguments(input); switch (object) { - case "int": + case "int" -> { return Ints.tryParse(toCheck) != null; - case "double": + } + case "double" -> { return Doubles.tryParse(toCheck) != null; - case "player": + } + case "player" -> { try { UUID id = UUID.fromString(toCheck); return Bukkit.getPlayer(id) != null; } catch (IllegalArgumentException e) { return Bukkit.getPlayerExact(toCheck) != null; } - case "uuid": + } + case "uuid" -> { try { UUID.fromString(toCheck); return true; } catch (IllegalArgumentException e) { return false; } - default: + } + default -> { holder.getPlugin().debug(DebugLevel.HIGHEST, Level.INFO, "Invalid object: " + object + " in \"is object\" check."); return false; + } } } } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java index 56a67760..0958d0cd 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java @@ -3,10 +3,10 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.utils.DebugLevel; -import java.util.logging.Level; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicePriority; @@ -27,8 +27,8 @@ public JavascriptRequirement(final @NotNull DeluxeMenus plugin, String expressio this.expression = expression; if (engine == null) { if (manager.isProvidedFor(ScriptEngineManager.class)) { - final RegisteredServiceProvider provider = manager.getRegistration(ScriptEngineManager.class); - engine = (ScriptEngineManager) provider.getProvider(); + final RegisteredServiceProvider provider = manager.getRegistration(ScriptEngineManager.class); + engine = provider.getProvider(); } else { engine = new ScriptEngineManager(); manager.register(ScriptEngineManager.class, engine, plugin, ServicePriority.Highest); diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementType.java b/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementType.java index d36213f4..61ce60b1 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementType.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementType.java @@ -31,10 +31,10 @@ public enum RequirementType { Arrays.asList("amount", "placeholder")), HAS_EXP(Arrays.asList("has exp", "hasexp", "exp"), "Checks if a player has enough exp", - Arrays.asList("amount")), + List.of("amount")), DOES_NOT_HAVE_EXP(Arrays.asList("!has exp", "!hasexp", "!exp"), "Checks if a player has enough exp", - Arrays.asList("amount")), + List.of("amount")), HAS_PERMISSION(Arrays.asList("has permission", "has perm", "haspermission", "hasperm", "perm"), "Checks if a player has a specific permission", Collections.singletonList("permission")), DOES_NOT_HAVE_PERMISSION( @@ -86,10 +86,10 @@ public enum RequirementType { REGEX_DOES_NOT_MATCH(Arrays.asList("!regex matches", "!regex"), "Checks if a placeholder parsed string does not match a regex pattern", Arrays.asList("input", "regex")), - STRING_LENGTH(Arrays.asList("string length"), + STRING_LENGTH(List.of("string length"), "Checks if the given string's length is between the provided minimum and (optionally) maximum.", Arrays.asList("input", "min", "max")), - IS_OBJECT(Arrays.asList("is object"), + IS_OBJECT(List.of("is object"), "Checks if the given string can be parsed as a given Java object.", Arrays.asList("input", "object")); diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java new file mode 100644 index 00000000..adcb1cff --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.plugin.Plugin; + +/** Just modified BukkitRunnable */ +public abstract class UniversalRunnable implements Runnable { + MyScheduledTask task; + + public synchronized void cancel() throws IllegalStateException { + checkScheduled(); + task.cancel(); + } + + /** + * Returns true if this task has been cancelled. + * + * @return true if the task has been cancelled + * @throws IllegalStateException if task was not scheduled yet + */ + public synchronized boolean isCancelled() throws IllegalStateException { + checkScheduled(); + return task.isCancelled(); + } + + /** + * Schedules this in the Bukkit scheduler to run on next tick. + * + * @param plugin the reference to the plugin scheduling task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTask(Runnable) + */ + + public synchronized MyScheduledTask runTask(Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTask(this)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this in the Bukkit scheduler to run asynchronously. + * + * @param plugin the reference to the plugin scheduling task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskAsynchronously(Runnable) + */ + + public synchronized MyScheduledTask runTaskAsynchronously(Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskAsynchronously(this)); + } + + /** + * Schedules this to run after the specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskLater(Runnable, long) + */ + + public synchronized MyScheduledTask runTaskLater(Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskLater(this, delay)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to run asynchronously after the specified number of + * server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskLaterAsynchronously(Runnable, long) + */ + + public synchronized MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskLaterAsynchronously(this, delay)); + } + + /** + * Schedules this to repeatedly run until cancelled, starting after the + * specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @param period the ticks to wait between runs + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskTimer(Runnable, long, long) + */ + + public synchronized MyScheduledTask runTaskTimer(Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskTimer(this, delay, period)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to repeatedly run asynchronously until cancelled, + * starting after the specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task for the first + * time + * @param period the ticks to wait between runs + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskTimerAsynchronously(Runnable, long, long) + */ + + public synchronized MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskTimerAsynchronously(this, delay, period)); + } + + private void checkScheduled() { + if (task == null) { + throw new IllegalStateException("Not scheduled yet"); + } + } + + private void checkNotYetScheduled() { + if (task != null) { + throw new IllegalStateException("Already scheduled"); + } + } + + private MyScheduledTask setupTask(final MyScheduledTask task) { + this.task = task; + return task; + } + +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java new file mode 100644 index 00000000..8949a140 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler; + +import com.extendedclip.deluxemenus.scheduler.bukkit.BukkitScheduler; +import com.extendedclip.deluxemenus.scheduler.folia.FoliaScheduler; +import com.extendedclip.deluxemenus.scheduler.paper.PaperScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.utils.JavaUtil; +import org.bukkit.plugin.Plugin; + +public class UniversalScheduler { + private static final boolean IS_FOLIA = JavaUtil.classExists("io.papermc.paper.threadedregions.RegionizedServer"); + private static final boolean IS_CANVAS = JavaUtil.classExists("io.canvasmc.canvas.server.ThreadedServer"); + private static final boolean IS_EXPANDED_SCHEDULING_AVAILABLE = JavaUtil.classExists("io.papermc.paper.threadedregions.scheduler.ScheduledTask"); + + public static TaskScheduler getScheduler(Plugin plugin) { + return IS_FOLIA || IS_CANVAS ? new FoliaScheduler(plugin) : (IS_EXPANDED_SCHEDULING_AVAILABLE ? new PaperScheduler(plugin) : new BukkitScheduler(plugin)); + } + +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java new file mode 100644 index 00000000..b79deffb --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.bukkit; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +public class BukkitScheduledTask implements MyScheduledTask { + + BukkitTask task; + + boolean isRepeating; + + public BukkitScheduledTask(final BukkitTask task) { + this.task = task; + this.isRepeating = false; + } + + public BukkitScheduledTask(final BukkitTask task, boolean isRepeating) { + this.task = task; + this.isRepeating = isRepeating; + } + + @Override + public void cancel() { + task.cancel(); + } + + @Override + public boolean isCancelled() { + return task.isCancelled(); + } + + @Override + public Plugin getOwningPlugin() { + return task.getOwner(); + } + + @Override + public boolean isCurrentlyRunning() { + return Bukkit.getServer().getScheduler().isCurrentlyRunning(this.task.getTaskId()); //There's no other way. Fuck bukkit + } + + @Override + public boolean isRepeatingTask() { + return isRepeating; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java new file mode 100644 index 00000000..4c562dd0 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java @@ -0,0 +1,129 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.bukkit; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +public class BukkitScheduler implements TaskScheduler { + final Plugin plugin; + + public BukkitScheduler(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean isGlobalThread() { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public boolean isEntityThread(Entity entity) { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public boolean isRegionThread(Location location) { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public MyScheduledTask runTask(Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTask(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLater(Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, period)); + } + + //Useless? Or... + public MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTask(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, period)); + } + + @Override + public void execute(Runnable runnable) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, runnable); + } + + @Override + public void cancelTasks() { + Bukkit.getScheduler().cancelTasks(plugin); + } + + @Override + public void cancelTasks(Plugin plugin) { + Bukkit.getScheduler().cancelTasks(plugin); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java new file mode 100644 index 00000000..eb9f199d --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.folia; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.plugin.Plugin; + +public class FoliaScheduledTask implements MyScheduledTask { + private final ScheduledTask task; + + public FoliaScheduledTask(final ScheduledTask task) { + this.task = task; + } + + public void cancel() { + if (this.task == null) { + return; + } + + this.task.cancel(); + } + + public boolean isCancelled() { + return this.task == null || this.task.isCancelled(); + } + + public Plugin getOwningPlugin() { + return this.task == null ? null : this.task.getOwningPlugin(); + } + + public boolean isCurrentlyRunning() { + if (this.task == null) { + return false; + } + + final ScheduledTask.ExecutionState state = this.task.getExecutionState(); + return state == ScheduledTask.ExecutionState.RUNNING || state == ScheduledTask.ExecutionState.CANCELLED_RUNNING; + } + + public boolean isRepeatingTask() { + return this.task != null && this.task.isRepeatingTask(); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java new file mode 100644 index 00000000..cbc3d26c --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java @@ -0,0 +1,220 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.folia; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import io.papermc.paper.threadedregions.scheduler.AsyncScheduler; +import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler; +import io.papermc.paper.threadedregions.scheduler.RegionScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.TimeUnit; + +public class FoliaScheduler implements TaskScheduler { + + final Plugin plugin; + + public FoliaScheduler(Plugin plugin) { + this.plugin = plugin; + } + + private final RegionScheduler regionScheduler = Bukkit.getServer().getRegionScheduler(); + private final GlobalRegionScheduler globalRegionScheduler = Bukkit.getServer().getGlobalRegionScheduler(); + private final AsyncScheduler asyncScheduler = Bukkit.getServer().getAsyncScheduler(); + + @Override + public boolean isGlobalThread() { + return Bukkit.getServer().isGlobalTickThread(); + } + + @Override + public boolean isTickThread() { + return Bukkit.getServer().isPrimaryThread(); // The Paper implementation checks whether this is a tick thread, this method exists to avoid confusion. + } + + @Override + public boolean isEntityThread(Entity entity) { + return Bukkit.getServer().isOwnedByCurrentRegion(entity); + } + + @Override + public boolean isRegionThread(Location location) { + return Bukkit.getServer().isOwnedByCurrentRegion(location); + } + + @Override + public MyScheduledTask runTask(Runnable runnable) { + return new FoliaScheduledTask(globalRegionScheduler.run(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(runnable); + } + return new FoliaScheduledTask(globalRegionScheduler.runDelayed(plugin, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(globalRegionScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return new FoliaScheduledTask(globalRegionScheduler.run(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(plugin, runnable); + } + return new FoliaScheduledTask(globalRegionScheduler.runDelayed(plugin, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(globalRegionScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Location location, Runnable runnable) { + return new FoliaScheduledTask(regionScheduler.run(plugin, location, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Location location, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(runnable); + } + return new FoliaScheduledTask(regionScheduler.runDelayed(plugin, location, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Location location, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(regionScheduler.runAtFixedRate(plugin, location, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Entity entity, Runnable runnable) { + return new FoliaScheduledTask(entity.getScheduler().run(plugin, task -> runnable.run(), null)); + } + + @Override + public MyScheduledTask runTaskLater(Entity entity, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(entity, runnable); + } + return new FoliaScheduledTask(entity.getScheduler().runDelayed(plugin, task -> runnable.run(), null, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Entity entity, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(entity.getScheduler().runAtFixedRate(plugin, task -> runnable.run(), null, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Runnable runnable) { + return new FoliaScheduledTask(asyncScheduler.runNow(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runDelayed(plugin, task -> runnable.run(), delay * 50L, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period) { + return new FoliaScheduledTask(asyncScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay * 50, period * 50, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return new FoliaScheduledTask(asyncScheduler.runNow(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runDelayed(plugin, task -> runnable.run(), delay * 50L, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay * 50, period * 50, TimeUnit.MILLISECONDS)); + } + + @Override + public void execute(Runnable runnable) { + globalRegionScheduler.execute(plugin, runnable); + } + + @Override + public void execute(Location location, Runnable runnable) { + regionScheduler.execute(plugin, location, runnable); + } + + @Override + public void execute(Entity entity, Runnable runnable) { + entity.getScheduler().execute(plugin, runnable, null, 1L); + } + + @Override + public void cancelTasks() { + globalRegionScheduler.cancelTasks(plugin); + asyncScheduler.cancelTasks(plugin); + } + + @Override + public void cancelTasks(Plugin plugin) { + globalRegionScheduler.cancelTasks(plugin); + asyncScheduler.cancelTasks(plugin); + } + + private long getOneIfNotPositive(long x) { + return x <= 0 ? 1L : x; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java new file mode 100644 index 00000000..6920ff97 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.paper; + +import com.extendedclip.deluxemenus.scheduler.folia.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +// Thanks to Towny +public class PaperScheduler extends FoliaScheduler { + public PaperScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public boolean isGlobalThread() { + // isGlobalThread does not exist on paper, match the bukkit task scheduler's behaviour. + return Bukkit.getServer().isPrimaryThread(); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java new file mode 100644 index 00000000..22b77198 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java @@ -0,0 +1,346 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.scheduling.schedulers; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +public interface TaskScheduler { + + /** + * Folia: Returns whether the current thread is ticking the global region
+ * Paper & Bukkit: Returns {@link org.bukkit.Server#isPrimaryThread} + */ + boolean isGlobalThread(); + + /** + * @return {@link org.bukkit.Server#isPrimaryThread} + */ + default boolean isTickThread() { + return Bukkit.getServer().isPrimaryThread(); + } + + /** + * Folia & Paper: Returns whether the current thread is ticking a region and that the region + * being ticked owns the specified entity. Note that this function is the only appropriate method of + * checking for ownership of an entity, as retrieving the entity's location is undefined unless the + * entity is owned by the current region + *

+ * Bukkit: returns {@link org.bukkit.Server#isPrimaryThread} + * + * @param entity Specified entity + */ + boolean isEntityThread(Entity entity); + + /** + * Folia & Paper: Returns whether the current thread is ticking a region and that the region + * being ticked owns the chunk at the specified world and block position as included in the specified location + *

+ * Bukkit: returns {@link org.bukkit.Server#isPrimaryThread} + * + * @param location Specified location, must have a non-null world. + */ + boolean isRegionThread(Location location); + + /** + * Schedules a task to be executed on the next tick
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + */ + MyScheduledTask runTask(Runnable runnable); + + /** + * Schedules a task to be executed after the specified delay in ticks
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + * @param delay The delay, in ticks + */ + MyScheduledTask runTaskLater(Runnable runnable, long delay); + + /** + * Schedules a repeating task to be executed after the initial delay with the specified period
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period); + + /** + * Deprecated: use {@link #runTask(Runnable)} + */ + @Deprecated + default MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return runTask(runnable); + } + + /** + * Deprecated: use {@link #runTaskLater(Runnable, long)} + */ + @Deprecated + default MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Deprecated: use {@link #runTaskTimer(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location on the next tick + *

+ * Bukkit: same as {@link #runTask(Runnable)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + */ + default MyScheduledTask runTask(Location location, Runnable runnable) { + return runTask(runnable); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location after the + * specified delay in ticks + *

+ * Bukkit: same as {@link #runTaskLater(Runnable, long)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + * @param delay The delay, in ticks. + */ + default MyScheduledTask runTaskLater(Location location, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Folia & Paper: Schedules a repeating task to be executed on the region which owns the location + * after the initial delay with the specified period + *

+ * Bukkit: same as {@link #runTaskTimer(Runnable, long, long)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + default MyScheduledTask runTaskTimer(Location location, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Deprecated: use {@link #runTaskLater(Runnable, long)} + */ + @Deprecated + default MyScheduledTask scheduleSyncDelayedTask(Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Deprecated: use {@link #execute(Runnable)} or {@link #runTask(Runnable)} + */ + @Deprecated + default MyScheduledTask scheduleSyncDelayedTask(Runnable runnable) { + return runTask(runnable); + } + + /** + * Deprecated: use {@link #runTaskTimer(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask scheduleSyncRepeatingTask(Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location + * of given entity on the next tick + *

+ * Bukkit: same as {@link #runTask(Runnable)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + */ + default MyScheduledTask runTask(Entity entity, Runnable runnable) { + return runTask(runnable); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location + * of given entity after the specified delay in ticks + *

+ * Bukkit: same as {@link #runTaskLater(Runnable, long)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + * @param delay The delay, in ticks. + */ + default MyScheduledTask runTaskLater(Entity entity, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Folia & Paper: Schedules a repeating task to be executed on the region which owns the + * location of given entity after the initial delay with the specified period + *

+ * Bukkit: same as {@link #runTaskTimer(Runnable, long, long)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + default MyScheduledTask runTaskTimer(Entity entity, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Schedules the specified task to be executed asynchronously immediately + * + * @param runnable The task to execute + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskAsynchronously(Runnable runnable); + + /** + * Schedules the specified task to be executed asynchronously after the time delay has passed + * + * @param runnable The task to execute + * @param delay The time delay to pass before the task should be executed + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay); + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, + * and then periodically executed with the specified period + * + * @param runnable The task to execute + * @param delay The time delay to pass before the first execution of the task, in ticks + * @param period The time between task executions after the first execution of the task, in ticks + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period); + + /** + * Deprecated: use {@link #runTaskAsynchronously(Runnable)} + */ + @Deprecated + default MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return runTaskAsynchronously(runnable); + } + + /** + * Deprecated: use {@link #runTaskLaterAsynchronously(Runnable, long)} + */ + @Deprecated + default MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + return runTaskLaterAsynchronously(runnable, delay); + } + + /** + * Deprecated: use {@link #runTaskTimerAsynchronously(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + return runTaskTimerAsynchronously(runnable, delay, period); + } + + /** + * Calls a method on the main thread and returns a Future object. This task will be executed + * by the main(Bukkit)/global(Folia&Paper) server thread. + *

+ * Note: The Future.get() methods must NOT be called from the main thread. + *

+ * Note2: There is at least an average of 10ms latency until the isDone() method returns true. + * + * @param task Task to be executed + */ + default Future callSyncMethod(final Callable task) { + CompletableFuture completableFuture = new CompletableFuture<>(); + execute(() -> { + try { + completableFuture.complete(task.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return completableFuture; + } + + /** + * Schedules a task to be executed on the global region + * + * @param runnable The task to execute + */ + void execute(Runnable runnable); + + /** + * Schedules a task to be executed on the region which owns the location + * + * @param location The location which the region executing should own + * @param runnable The task to execute + */ + default void execute(Location location, Runnable runnable) { + execute(runnable); + } + + /** + * Schedules a task to be executed on the region which owns the location of given entity + * + * @param entity The entity which location the region executing should own + * @param runnable The task to execute + */ + default void execute(Entity entity, Runnable runnable) { + execute(runnable); + } + + /** + * Attempts to cancel all tasks scheduled by this plugin + */ + void cancelTasks(); + + /** + * Attempts to cancel all tasks scheduled by the specified plugin + * + * @param plugin specified plugin + */ + void cancelTasks(Plugin plugin); +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java new file mode 100644 index 00000000..47ae40fa --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java @@ -0,0 +1,54 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.scheduling.tasks; + +import org.bukkit.plugin.Plugin; + +public interface MyScheduledTask { + + /** + * Cancels executing task + */ + void cancel(); + + /** + * @return true if task is cancelled, false otherwise + */ + boolean isCancelled(); + + /** + * @return The plugin under which the task was scheduled. + */ + Plugin getOwningPlugin(); + + /** + * @return true if task is currently executing, false otherwise + */ + boolean isCurrentlyRunning(); + + /** + * @return true if task is repeating, false otherwise + */ + boolean isRepeatingTask(); +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java new file mode 100644 index 00000000..b5b85152 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java @@ -0,0 +1,12 @@ +package com.extendedclip.deluxemenus.scheduler.utils; + +public class JavaUtil { + public static boolean classExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java index e30fe77b..4ebd0d6d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java +++ b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.listener.Listener; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; import java.io.BufferedReader; @@ -15,7 +16,6 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; public class UpdateChecker extends Listener { @@ -25,28 +25,20 @@ public class UpdateChecker extends Listener { private static final TextReplacementConfig.Builder CURRENT_VERSION_REPLACER_BUILDER = TextReplacementConfig.builder().matchLiteral(""); - final int resourceId = 11734; + private static final int RESOURCE_ID = 11734; + private final TaskScheduler scheduler; private String latestVersion = null; private boolean updateAvailable = false; public UpdateChecker(final @NotNull DeluxeMenus instance) { super(instance); + this.scheduler = plugin.getScheduler(); - new BukkitRunnable() { - @Override - public void run() { - if (check()) { - new BukkitRunnable() { - - @Override - public void run() { - register(); - } - }.runTask(plugin); - } + scheduler.runTaskAsynchronously(() -> { + if (check()) { + scheduler.runTask(this::register); } - - }.runTaskAsynchronously(plugin); + }); } @EventHandler(priority = EventPriority.MONITOR) @@ -74,7 +66,7 @@ public void onJoin(final @NotNull PlayerJoinEvent event) { private String getSpigotVersion() { try { HttpURLConnection connection = (HttpURLConnection) new URL( - "https://api.spigotmc.org/legacy/update.php?resource=" + resourceId).openConnection(); + "https://api.spigotmc.org/legacy/update.php?resource=" + RESOURCE_ID).openConnection(); connection.setDoOutput(true); connection.setRequestMethod("GET"); return new BufferedReader(new InputStreamReader(connection.getInputStream())).readLine(); diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/DebugLevel.java b/src/main/java/com/extendedclip/deluxemenus/utils/DebugLevel.java index d21e06a7..d89e701f 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/DebugLevel.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/DebugLevel.java @@ -17,7 +17,7 @@ public enum DebugLevel { private final String[] names; private final int priority; - private DebugLevel(final int priority, @NotNull final String... names) { + DebugLevel(final int priority, @NotNull final String... names) { this.priority = priority; this.names = names; } diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/ExpUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/ExpUtils.java index e1d0f4f1..023a5cc6 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/ExpUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/ExpUtils.java @@ -26,7 +26,7 @@ public static void setExp( final String lowerCase = stringAmount.toLowerCase(Locale.ENGLISH); if (stringAmount.contains("l")) { - final int neededLevel = Integer.parseInt(lowerCase.replaceAll("l", "")) + target.getLevel(); + final int neededLevel = Integer.parseInt(lowerCase.replace("l", "")) + target.getLevel(); amount = getExpToLevel(neededLevel) + (getTotalExperience(target) - getExpToLevel(target.getLevel())); setTotalExperience(target, 0); } diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java index 870e8312..08de3186 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java @@ -33,7 +33,7 @@ public static boolean isPlaceholderOption(@NotNull final String material) { /** * Checks if the string starts with the substring "stack-". The check is case-insensitive. * - * @param itemstack The string to check + * @param material The string to check * @return true if the string starts with "stack-", false otherwise */ public static boolean isItemStackOption(@NotNull final String material) { diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/Pair.java b/src/main/java/com/extendedclip/deluxemenus/utils/Pair.java index 3ba367cc..78bd7a43 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/Pair.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/Pair.java @@ -2,11 +2,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; -public class Pair { - - private final K key; - private final V value; +public record Pair(K key, V value) { public static Pair of(@NotNull final K key, @Nullable final V value) { return new Pair<>(key, value); @@ -21,16 +19,18 @@ public Pair(@NotNull final K key, @Nullable final V value) { this.value = value; } - public @NotNull K getKey() { + @Override + public @NotNull K key() { return key; } - public @Nullable V getValue() { + @Override + public @Nullable V value() { return value; } @Override - public String toString() { + public @NonNull String toString() { return "Pair{" + "key=" + key + ", value=" + key + '}'; } } diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/SkullUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/SkullUtils.java index 38ad9457..c25c64f7 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/SkullUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/SkullUtils.java @@ -81,8 +81,7 @@ public static ItemStack getSkullByBase64EncodedTextureUrl(@NotNull final DeluxeM } public static String getTextureFromSkull(final DeluxeMenus plugin, ItemStack item) { - if (!(item.getItemMeta() instanceof SkullMeta)) return null; - SkullMeta meta = (SkullMeta) item.getItemMeta(); + if (!(item.getItemMeta() instanceof SkullMeta meta)) return null; if (VersionHelper.HAS_PLAYER_PROFILES) { PlayerProfile profile = meta.getOwnerProfile(); @@ -150,8 +149,7 @@ public static ItemStack getSkullByName(@NotNull final DeluxeMenus plugin, @NotNu } public static String getSkullOwner(ItemStack skull) { - if (skull == null || !(skull.getItemMeta() instanceof SkullMeta)) return null; - SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (skull == null || !(skull.getItemMeta() instanceof SkullMeta meta)) return null; if (!VersionHelper.IS_SKULL_OWNER_LEGACY) { if (meta.getOwningPlayer() == null) return null; @@ -183,7 +181,7 @@ private static GameProfile getGameProfile(@NotNull final String base64Url) { */ @NotNull private static PlayerProfile getPlayerProfile(@NotNull final DeluxeMenus plugin, @NotNull final String base64Url) { - final PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID()); + final PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); final String decodedBase64 = decodeSkinUrl(base64Url); if (decodedBase64 == null) { diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index 6ae95804..81fd30f8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - import me.clip.placeholderapi.PlaceholderAPI; import net.md_5.bungee.api.ChatColor; import org.bukkit.Color; diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/VersionHelper.java b/src/main/java/com/extendedclip/deluxemenus/utils/VersionHelper.java index 87e9a943..a676f056 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/VersionHelper.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/VersionHelper.java @@ -185,7 +185,6 @@ private static int getCurrentVersion() { else stringBuilder.append(patch.replace(".", "")); } - //noinspection UnstableApiUsage final Integer version = Ints.tryParse(stringBuilder.toString()); // Should never fail diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml new file mode 100644 index 00000000..6a38f0e2 --- /dev/null +++ b/src/main/resources/paper-plugin.yml @@ -0,0 +1,55 @@ +name: DeluxeMenus +main: com.extendedclip.deluxemenus.DeluxeMenus +version: '${version}' +api-version: '1.21' +folia-supported: true +dependencies: + server: + PlaceholderAPI: + load: BEFORE + required: true + join-classpath: true + Vault: + load: BEFORE + required: false + join-classpath: true + HeadDatabase: + load: BEFORE + required: false + join-classpath: true + CraftEngine: + load: BEFORE + required: false + join-classpath: true + ItemsAdder: + load: BEFORE + required: false + join-classpath: true + Nexo: + load: BEFORE + required: false + join-classpath: true + Oraxen: + load: BEFORE + required: false + join-classpath: true + MMOItems: + load: BEFORE + required: false + join-classpath: true + ExecutableItems: + load: BEFORE + required: false + join-classpath: true + ExecutableBlocks: + load: BEFORE + required: false + join-classpath: true + Score: + load: BEFORE + required: false + join-classpath: true + SimpleItemGenerator: + load: BEFORE + required: false + join-classpath: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c57c869c..ec50f892 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,8 @@ -api-version: 1.13 name: DeluxeMenus main: com.extendedclip.deluxemenus.DeluxeMenus version: ${version} +api-version: 1.13 +folia-supported: true authors: [ HelpChat ] softdepend: [ PlaceholderAPI, Vault, HeadDatabase, CraftEngine, ItemsAdder, Nexo, Oraxen, ExecutableItems, ExecutableBlocks, Score, SimpleItemGenerator, MMOItems ] description: All in one inventory menu system