From de96bd314f6f7d9067ead672da0924121a9de502 Mon Sep 17 00:00:00 2001 From: Kilka_v_HJlebe Date: Sun, 2 Aug 2026 22:21:03 +0300 Subject: [PATCH] feat: pizdec --- .gitignore | 4 +- .gradle/9.0.0/checksums/checksums.lock | Bin 17 -> 17 bytes .gradle/9.0.0/checksums/md5-checksums.bin | Bin 25747 -> 30647 bytes .gradle/9.0.0/checksums/sha1-checksums.bin | Bin 45065 -> 78971 bytes .../executionHistory/executionHistory.bin | Bin 251776 -> 142519 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 17 bytes .gradle/9.0.0/fileHashes/fileHashes.bin | Bin 25147 -> 26397 bytes .gradle/9.0.0/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../9.0.0/fileHashes/resourceHashesCache.bin | Bin 20945 -> 20299 bytes .gradle/9.2.1/checksums/checksums.lock | Bin 17 -> 0 bytes .../executionHistory/executionHistory.bin | Bin 44995 -> 0 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 0 bytes .gradle/9.2.1/fileChanges/last-build.bin | Bin 1 -> 0 bytes .gradle/9.2.1/fileHashes/fileHashes.bin | Bin 19647 -> 0 bytes .gradle/9.2.1/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../9.2.1/fileHashes/resourceHashesCache.bin | Bin 18769 -> 0 bytes .gradle/9.2.1/gc.properties | 0 .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/buildOutputCleanup/cache.properties | 2 +- .gradle/buildOutputCleanup/outputFiles.bin | Bin 19019 -> 19631 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .../AdminMode-1632350505/project-info.ser | Bin 156432 -> 0 bytes .gradle/nb-cache/subprojects.ser | Bin 664 -> 0 bytes ...069E2295F6FA6FA2C78DAF19CD6F43A89A11C6725E | 1 - .idea/.gitignore | 0 .idea/compiler.xml | 0 .idea/easycode.ignore | 0 .idea/gradle.xml | 0 .idea/misc.xml | 0 .idea/modules.xml | 0 .idea/modules/AdminMode.main.iml | 1 - .idea/vcs.xml | 6 + LUCKPERMS_SETUP.md | 234 +++ MODERATION_PLUGIN_CONTRACT.md | 149 ++ README.md | 68 + bin/main/config.yml | 56 +- bin/main/plugin.yml | 17 +- build.gradle | 13 +- gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 0 gradlew.bat | 188 +-- src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/main/java/.DS_Store | Bin 0 -> 6148 bytes src/main/java/org/.DS_Store | Bin 0 -> 6148 bytes src/main/java/org/blz/.DS_Store | Bin 0 -> 6148 bytes .../java/org/blz/adminmode/AdminMode.java | 34 +- .../adminmode/commands/AdminFixCommand.java | 41 +- .../adminmode/commands/AdminModeCommand.java | 483 ++++-- .../blz/adminmode/config/ConfigManager.java | 436 ++--- .../adminmode/data/AdminModeDataManager.java | 86 +- .../adminmode/dialog/AdminDialogService.java | 1479 ++++++++++++----- .../dialog/DialogPreferenceStore.java | 63 + .../dialog/MenuEmojiPreferenceStore.java | 72 + .../integrations/CoreProtectIntegration.java | 113 +- .../integrations/LuckPermsIntegration.java | 65 +- .../listeners/AdminModeListener.java | 15 +- .../listeners/GameModeChangeListener.java | 49 +- .../listeners/MobTargetListener.java | 0 .../listeners/PlayerDeathListener.java | 2 +- .../listeners/PlayerSessionListener.java | 3 +- .../adminmode/managers/BelzeBoolManager.java | 60 +- .../blz/adminmode/moderation/ModeManager.java | 333 ++-- .../blz/adminmode/moderation/ModeSession.java | 2 +- .../moderation/ModeSessionLogger.java | 2 +- .../moderation/ModeratorAccessManager.java | 280 ++++ .../moderation/ModeratorPreferencesStore.java | 230 +++ .../moderation/ModeratorProfile.java | 54 +- .../moderation/ModeratorWarningStore.java | 256 +++ .../moderation/PlayerFreezeManager.java | 127 ++ .../moderation/PlayerListAction.java | 3 +- .../moderation/PunishmentDuration.java | 57 + .../blz/adminmode/session/SessionStorage.java | 90 +- .../blz/adminmode/utils/ItemStackCodec.java | 85 + .../adminmode/utils/PotionEffectCodec.java | 86 + .../blz/adminmode/utils/ScreenEffects.java | 0 .../org/blz/adminmode/utils/WorldLoader.java | 59 + src/main/resources/config.yml | 107 +- src/main/resources/plugin.yml | 41 +- src/main/resources/pocket_dimension.zip | Bin 80 files changed, 4121 insertions(+), 1431 deletions(-) mode change 100644 => 100755 .gitignore delete mode 100644 .gradle/9.2.1/checksums/checksums.lock delete mode 100644 .gradle/9.2.1/executionHistory/executionHistory.bin delete mode 100644 .gradle/9.2.1/executionHistory/executionHistory.lock delete mode 100644 .gradle/9.2.1/fileChanges/last-build.bin delete mode 100644 .gradle/9.2.1/fileHashes/fileHashes.bin delete mode 100644 .gradle/9.2.1/fileHashes/fileHashes.lock delete mode 100644 .gradle/9.2.1/fileHashes/resourceHashesCache.bin delete mode 100644 .gradle/9.2.1/gc.properties delete mode 100644 .gradle/nb-cache/AdminMode-1632350505/project-info.ser delete mode 100644 .gradle/nb-cache/subprojects.ser delete mode 100644 .gradle/nb-cache/trust/F67B3012358146C8F2D19F069E2295F6FA6FA2C78DAF19CD6F43A89A11C6725E mode change 100644 => 100755 .idea/.gitignore mode change 100644 => 100755 .idea/compiler.xml mode change 100644 => 100755 .idea/easycode.ignore mode change 100644 => 100755 .idea/gradle.xml mode change 100644 => 100755 .idea/misc.xml mode change 100644 => 100755 .idea/modules.xml mode change 100644 => 100755 .idea/modules/AdminMode.main.iml create mode 100755 .idea/vcs.xml create mode 100644 LUCKPERMS_SETUP.md create mode 100644 MODERATION_PLUGIN_CONTRACT.md create mode 100644 README.md mode change 100644 => 100755 bin/main/config.yml mode change 100644 => 100755 bin/main/plugin.yml mode change 100644 => 100755 build.gradle mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.jar mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.properties mode change 100644 => 100755 gradlew.bat create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store create mode 100644 src/main/java/.DS_Store create mode 100644 src/main/java/org/.DS_Store create mode 100644 src/main/java/org/blz/.DS_Store mode change 100644 => 100755 src/main/java/org/blz/adminmode/AdminMode.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/commands/AdminFixCommand.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/commands/AdminModeCommand.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/config/ConfigManager.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/data/AdminModeDataManager.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/dialog/AdminDialogService.java create mode 100755 src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java create mode 100755 src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/listeners/AdminModeListener.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/listeners/MobTargetListener.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/moderation/ModeManager.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/moderation/ModeSession.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java create mode 100755 src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java create mode 100755 src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java create mode 100644 src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java create mode 100755 src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/moderation/PlayerListAction.java create mode 100644 src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/session/SessionStorage.java create mode 100644 src/main/java/org/blz/adminmode/utils/ItemStackCodec.java create mode 100644 src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/utils/ScreenEffects.java mode change 100644 => 100755 src/main/java/org/blz/adminmode/utils/WorldLoader.java mode change 100644 => 100755 src/main/resources/config.yml mode change 100644 => 100755 src/main/resources/plugin.yml mode change 100644 => 100755 src/main/resources/pocket_dimension.zip diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 5697b94..5400a79 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.DS_Store -/build \ No newline at end of file +/build +.idea +bin \ No newline at end of file diff --git a/.gradle/9.0.0/checksums/checksums.lock b/.gradle/9.0.0/checksums/checksums.lock index b9d96cccc93d202603682c5ea063e74863e6150f..2166cd97f7eee735dcd5d176821d6c4b56ba997f 100644 GIT binary patch literal 17 VcmZQ}nHc=*sCuUm0~j#x0RS)N1Z)5R literal 17 VcmZQ}nHc=*sCuUm0~jzJ0RS)T1aklY diff --git a/.gradle/9.0.0/checksums/md5-checksums.bin b/.gradle/9.0.0/checksums/md5-checksums.bin index 4affa5a09eedee6acc295f40659739322735cab2..cf5df9810e5a825b158a8098732eed66fd22c5cc 100644 GIT binary patch delta 4190 zcmZXXdt6NEAIF=mL>Dtmmzh&jGhH+;A%z(>W=m>Hx=&I`+g(lfXoqga^ZtH5-|zFB)A^ngTw*l7VAR=j z7=OcT&P?I@!F^rL#Y2v2sw3X^6LsWNTa0Py5DZ|RbPPf`eAcF7v6k(|1IsZ?y95dj zUy`4^*;E%Qr5L6xq%&-uA-QnMAR-_0__ojdYT)AUGKcdpPc4H2j+bz&_M@3EA||y_ znM-JK;fT}uHO&nbm`6^AE)HK zHxzS4tn&P`1^c~+x-g8}1+82$lLclP+N8A#mC9MU8 zv50kQ)$aqXi{95^m>vif$ULKD+}FnyS(rtgf-d9wfTLyC59`d-#4P#|y~WFm2HKM{ z;R@yvVKl!o$LQ_H!g0?qj|`%Ddu;m+L;s-?O8*9mO$1DimTB68uu_V(LaPaXS!cDw zPkvrSn5jis&V;z-VxB6d12-O?&fCA&zzXxIr_f`v zLvnY|$ES(HulX3JB-7%%Bd-?l91pF-JXS&Tdode!JHH!WgL(R9kn{MgKU_Mtmimue z#4ySqauDoy)!Sjv#^PbPsvOFAvCM#J83k6B-k8M~!v~&-H8{`nQP13{J`5uOY)ri* zFMZE;``WHqhG9%EU4O`y4N}^S9|ueGWka>Ryg@f(lbF)`&ys`oZU}TX2%d0(fp(C)7Ar zUiIVkGC78dP&$KVtH1P|=wbursc}$j#uttWly*Eg?%a!EG>;Z*xjWcOt#>zJo?rzv zX4w*LOHr_nTR}I5vA@$|9o8ScD^@?fjd>d5GjEn|HI8HUV=n(2toS09-b(H3$M%Y- zHAig(e?FhZluhDAY)u-(%DA~8=Zl$vO3RSB>es1i6QGPQU|tDnsjM~INmY6e4M?Z4 zFXc$n*cQxE^xzRnXb@>%T~$4W>Siei-~-=_W&A(;@{LKg)a@Y;0cSxB%e3~6$kumK zHnzvbLWN+5(28Z#s;)3Sh+$G6y(817Q^cOUrcBXRmKU%%Im?v?;!(6}|hE2Q~DWq2v`6o$!% zL1D=kt}NR(wxROzdJGk>>H1;$cNT`bIjpAmc_^^tv!ePE7H{bapgKdE9aLCmvyzH_ z-EzPYK4N8z6nIvAR%)Mg6L>{NVW{+l*;WGP_|h#E75qHhChQKZkC zETJ#K8OP3qMt?G+z7O>Ilgnhk2DXgc2|Ec(bku;M+KK)NnCe4-bpV-K|2`}XAVA?p zP7J0&fg1s#GF9rk*rzLjei@nCU>@ipX(0Cf2FYmhB7l?{#9*o3Gsu#W8`a0p6$Xy&#^@2)sRo=`5QzslP=W`YkQG8ee;~O#t}(nb*{X@j z8}L9AC)H)+aUlp@wo)AV0QD#WD@c#2EgW4zRyQqzo)rWXgp!xb^eZ;>rYS0|9>`iq!?j`-m}pTyQ&DkF?Jfw&i5FLGHn*W#EvBL zJU1!I|0FBEPSmcP{yM*0*;T+NM*%+o+omSt)k|y!z#iE(_KTWQBO0+lqtfEY%{_ z*hfdNa(`jlu?q?1TMDmjxL~*W#yj4X7Ws= zlW)Z{(8p+Gv_&&vP(FRT+qk;+{9pF9xNoN{m&lI5`IVEao|F0P(^=2;;gu|Y6%H!3 zQMV&FKj^$vTW?oj^ze@K_VRz$^-)17>VO0%HL~c=`3HuB+kU%uy*+yx>e1v%lzFHt z5*%B`Z2hjUx$Alr^S6I>Q9O|f4x)}ia9)`#8M~>xU7T=D645kv_%aS=Sdm?Zv;D93 zHuvnuzBbLZH4e3=9aJzLwF$yfH~8c)&*kx=#1NSev4DXd+hom*BWTm;FqELL>Hh(! C%I2d0 delta 433 zcmXX>O(;ZR6n#^sFyk-rzUO(&n=x<9Jnzjn`B^Hd@hm1}Mpi<`Mu|jbH)SIWKb>Z0 zHp<_Qkd=irLa`Ex>?8|GQnJ7uoI3a1?mhS3?*5Anr?#yIW?Mod<47@v4ACoDNJ|V4 z5{FU2u}ZYd3=x?;atQabh33j@7>a~Gg=0-&xKe^pszMm9+9*n#4DPKwI)z^h1re`S zi(bFFlKB@C|Fr?6s$D2^E#le5ps2K49l)^~M1h-byE)1{7A8F#h}1BoYNiqMa-_Ws z=iU&?HHNUpF|Ua@&@3pvex!Vhh}BZRR>Bu)*e_zz&#>y3a7DR2(2n7Nh)h62p)Mk( z(>|U2I{Ec=9Mmz`4TeTz8X3}eLxN@okv1*o_df)oU~^mj5V!eQ{!WY4>aZ_I5;?a} z;zl-en&^m|J5Sxw;-2VHa<}(Y==_`&&q~JSJ3+85FYxl*zyC|aq-H$@KT*xHc>dDS ccA7WEMh`n?-u50!vlBOSZw_h_nnoy?KjBl7#{d8T diff --git a/.gradle/9.0.0/checksums/sha1-checksums.bin b/.gradle/9.0.0/checksums/sha1-checksums.bin index aac8d911259caea4c6ab0eb4eaea81a69b9cf496..342fb2c371a32d14463152dbdd2dfecc429d9795 100644 GIT binary patch delta 13964 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 delta 7800 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

    V18mpPlGMHKMbA_1X@q&Kop)RURFQT`9+}SU~ z#w_SKoxl^2xMSj2Am~E8ege*?E)DG)m6jT~;^DrBeHXlcB>4&5t=dSui0ZXVi;}4} zWvWdV&>0C0^rXTO9U-FHV zUFH7{o(pT<>-G4v9sL$g!xHZQeEPLag_5Z@WvWdyWnz`1J{kN}aXjkK@x`d{ zN1+byS=QcouC(1AS zE>msFRGYrx-I>U<79j-8xps8hO?TdoLXA8+ zi3i!9FEz9AgG% z;`=LKx5Oq)rrMOLHhpZMmn##`O0I2NuEdR!;cs_4{P?d>N02}#+3V1zVaRmvFv(P# zI+HkfJFIy-{AQcprI_{XvV%|G9-e!vE17E3Ka~kv`RUabO%7gema=>AsD{SEO<&bS zCQPQVc^(iiK~a5Ew?*-z zDkMzobCWA`xaQKuAxHK7)`uux$&*8)eR;`CGS#NX=vF(8j&n)@4iX?u1Od39abSa_ zNmgev0{uL1q99YA;LLEOm?K%%K>E%o1WkhyPw|LJ6EfANmnSnPla^$vO{Yw?nQ^)j z_QlLfk2KZu4X)ZEywA`=oatp#ZB~@2Hf5^KP{=CM<;qlJTSsG+4dLaQ-tC%qL+d)H zf0n5>fd(Wq$_h6Xq=+WpM5Nv9;Lce_kd$idzVpw&ChY3artfb}Be`XfKLzCl&5*7$ z2kC&^%1U~+b)@CURAYzx;}4ooDez`FZus^2%N%2r>mf1{z^|LB7@2C5*QR2sY;1S% zz%kWX9Y-}ctUbP9n!xHt5@0pv9t3Ej09Vl3E?&rjxXOr70YJO4tD4zdGKo> z9KmOMAX~^RFnZPI=vAAbw4#qK!GtFkJ$lt91#J$bkcCuWqgQPruG*wxdjmO~OY<^|Gjdb&N?b}& zIswbRPj>0c{d4y2Z`WC8{y*G+t-V3m94cnJV#qaOqxXZ3-Ve$wpfz#h>GU<~wGSLV zafnT^n9R5uvtEE)gBqM%l$x673OoS_c%3S^gZqGb4R?qYgW`Tr4YNB8uQWDRn74lt z-V{>3eemB8di3bexn$lCx*V*PwEIC1><3v#%Kf1Cz&gmfAC!IZ_ixw4%bMrSdF8xQ z%x6v80g&Z4`Wb;2LfK8Tt&uD9nRwpePOHhMlZp&fxFB>ZI6z3fAan;<7X=rD=GIJ2 zynX!T{-|lfyMJ-Z)SW&I^5zyE##NZ(k17H1s>Tsgt3mHwW9GP&zK6Bt6Z1bQp${9I zmYe_^vQr9r`77!mCRC4FtOMiv(^WqX4A33*-z6I+9-!fXU>gYb3<)T97ZGV2ff3$l#Lof1B72{kSgKbbJ zJz&3@?X@AOEq~#S9KIc2TyBDs3aPh@9zOyKc`DvAIt!dONWW#Y8=PrKx@EKtY!!*O zj5dQck$lVOT(CAO-ZHxRASmETy=8PeSQn|cjP3yIBJGyZonSqr-7>lxtcR3aM)!er zQ0tb_qu?w+2AH%jjFMNo3qI3f4#QEu&rFEKHqSMmxY}QFzN}C)g0`-7?w& zHjk`ZM!O;YA>o!$Nclk8Eu(>J|GA`oOPF+;SNUPuq(3T~!M3g!V_fA68Q;n;LLQ?~ zfpZc|fJfIr%kx>`#f#3IDf@eM!lzyKTd(Fr47tJ0I0vgCFm2rXr)|CPWufBf2g^8m z{;cEL3ATGpG*MTT{sAi{>8jHAU@atGReBtp%}Kec6jH&?<6@kNsQ%$SO=yUvN9^Hv z8dXxkt$AzpyYqiz_k+vmqoTmJQq|q&$3Gv4D_7o7-dmf#{VK`Vn?3}4lN#5Xe%%Wy z=t;WXv~?dy%UR;DH$4WngXrr`A~>en-1dZO}poPH;-Z9h>@7~C(5mHzs~7npaeR51^9Z?lG^}Hhxb|l literal 251776 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 RQCCijt>NEoVpm{(`9Guf)0zMP diff --git a/.gradle/9.0.0/executionHistory/executionHistory.lock b/.gradle/9.0.0/executionHistory/executionHistory.lock index e39881beeeeb07d671a08a5e05de744e148b121a..83a130f5c159d3894e57159118edd0ae81adc314 100644 GIT binary patch literal 17 TcmZQ}N`7eD*Hmr500D^rEVTpU literal 17 UcmZQ}N`7eD*Hmr500xW_04#0;n*aa+ diff --git a/.gradle/9.0.0/fileHashes/fileHashes.bin b/.gradle/9.0.0/fileHashes/fileHashes.bin index c9397464600cba3df454a39f58cce7d9fe3e8c88..6616e342f4c4fa4263adc1d31cef026c312aad42 100644 GIT binary patch literal 26397 zcmeI4c{o+uAMlS%2Pqjcrj!gNM3GQb#>}&f$1G!}l2A%1niPdh(cqerC}oODG8Ii4 z47VgQB^sr7?X&jYH)o&sd47Mt?^(}t_wjsMpZ#6yyZ8R?rPjIINF*ViKWHQU*989i zk?|8F1B?tXGQh|HBLj>KFfzc%03!p83@|dl$N(b)j0`X`z{mh21B?tXGQh|HBLj>K z{J)X`YcLT`I2j?idB_h7caunp^T7{U`v>IGK8aBZ;U9ZCVE=v)HlyVn>$ z8kG}iRweQQa;F-MA4@t_tRyk74RV($^xl+||$va@V&wzd9D9>9i;Xa(8cxr|9dL zgxD&f=bN$nPhopCD`9t0^?~LbmNuvJYpcX!0?~6M;;{`qKnZyxQJqZkRi`q zF|xyg0{a`T$9YxbvAb%k!yuc?VsViW9#XUT{HAAAU5naR6UYtJF@CGbU#*u*EF5yH)i`fEXz3ht9zBmy9mcDSTK`%4-2uJ6 z=9)Nfiu_i%dpkcokG?j>t1rmEKecu_6Xf>X7_WJBu;|LN(h9VHD8_3q`x_O!KcE4* z0SC@IT2s7iYFHt6Z^3x|@r_gVthYBqZkdAdhI;KR2~xBIFfVF)3Aw2t)_-Kf#TEAs&$2;o&V}*O(XgAW z&C~jj8{*exbRw!$!RU4#T%2;ETk9+1DfuxyOBsvkL55oKawU5$*pU7yO`g?|rW% zX3WZ;oPb}^ZZ~5dY{-F z#5m`#{uQ~d`&wXs$I}?+s@HZWy*n)jIR&o|^Uv=3tm7E93UXs>tpCE?>e}1-XV5z4 z*og5(lF?g-b*$0*(8Lty%@#XqBm>a%*l)%-um56|)&jo{cz&mGjPo^mCnbCxNrT*w z8RG(Se(YnU5naf)Q*a*M?>D0!pA5MdKF-Bj_w2k)z0kU9yB+H<#L*P~Y4SSyoZ3d? zJl<09v+{zYXk5H*h`M|fbo&;Iy_Z?B{$gYG`_BBXMxPhUCX9=FbjEEEbwlf|%R8KZ zlMG4N;x7fy?}pdg75iWEw7m_QfZV(b>n~~gM6&GuHncudv@yQ&iTAVcO?qOm{|Jc^8$upe?xDamnlz;eb=*q+_ezn>iox6t8X)Zi1J9BJEWX* ze!l{JpIYC+xMplE*GOFi2kdXBgYnHt+(vu%V(%#r{CVF}&eo@_{tsH0EHbeETg&(l zri7{e3(sRcigBHu;|`gtgz6#pl*Rea^NlmluE|2a{Ts%Os#m@;Dc{rpxrHjm&F*>@ zcqYzPLhk5-aq~@uJ)pdum$5*vBmQ^Q%#~EH^S?l)$BX_ z{54C^I_!==cQ(gjPmFe6IEltxgPq5wZ_n9H3*U@F?tzcr)+&7e@BuY6&tCZbV_&&x zm(6HqAMCGx2|JI2(*2`)PsxFhTe@T1`9_iLP+|TgMC-G`6O0EWW)FYb z_24_~@1TtFkSwy5QRkZx$ZeKlJhXJG=5^v-wElQiVLU8vy=xV>Tq^AEhMzxN^2Om# z>%XD(#8w#VA1)R4(NDa`1@^c6f%7BsVK3K`{UCSM!}y-sZ-KE-$IyE2a1P^M!#`sj85v+?fRO=41{fJ&WPp(YMg|xeU}S)i z0Y(NG8DM09kpV^q7#Uz>fRO=41{fJ&WPp(YMg|xeU}S)i0Y(NG8DM09k%9kzGJtG; zK{mQbAph~~WgZX{W4fEYG-BPlVztnfBob3bo};TV%m-H__>`Y(#I{_T>TJmlD1Dt- zTk~!$*h@?`mO`WKG||xCCimsB!{b(N7Mb0{y(Ujk1KmACLUs$--r-!H>C60Bpt zTzg%YE7)&Gje+h9T8``&rZJ>s9&>-l5j09|(vSXQQ3*!Dl%de80|&+7XhSs4^lQ}? zzH8_7*rpX9AuwY*#|X@}BN~Q}_f+0>_bcO`E(t%s@08XYL)Zq{HA6G1cPt&p)~mA& z-Yxvps+M&a?6?1GRLniQh(@J#!=t2cS8RTIwmg;VR?nSduqq%seraNK?M+y8FjQ#u z_3NriPGg(E945F_>ephSU%ZgM5W?b-p06Mn7%t ze;<*W<`_b5T0}!lXuyJbXU?7*_gUn~`8E!J8}oejAvYk+Ja`taXn1aAzURP2xs=^y z(e{5EB$=AsL}S^+L1&Gsy>c`EG%V2_3jn*}z+GZ+3;mV|GdI!rb*ziiL+D}O~h?q2^8q@Dp4Nu#tB%Wt($#vvL9M#yaiS96^e!GUo;F^NqCTG^c&}@ z`PS%vI$B53m`R*>X|CbQL^QItnzrV?$*QTeHRE$SFse7#SgJ}iR0RDg*5uH;KF`PI zH(mWGf*5(;6kgC1G&T-!pqWSM-I0yPC%;elNPJJ(!|MTduu%;et5E&Y2%ioX1@wM328&`?44x6&A?!CgzwwYuc}Znxs<4YpW<8pvos6BYyq z8e>-dO1VaXQ|?gLqK}W=RANyB)Gefar;{tWr-Hfphh+KNublW*Q5U+LYM93$wGgy< zCg4C5!{7FVw6%<2kXbgzc*n~hV8$R<_SV`|fy6~;9&}@PQ=FmkKgaNxl;a z7*gucpclj7UAM=;6|U*P;m2MLnyL{X#`zML^*L07RTA7aw4+i_zuXva+qSF5@n{jn zp1W&~LF&3sH1vPGts_|$ln76VMX1Kw^UX0>$!CeiK-`9ZFIBU8-%Au*RX1<`yE%s7 z?khxN^wtv#F~0Jr@6{-`<)fKj&M{;x>AcluinXV0jt-qItFDta3JA-dV^s9H6JuO3 z6NzVQ4$8MsaDH;b`Dr3zoZ`Bw4|;+&aSt45uAu7IAH~7LXR1S6A2aU}UJss!zp_iB zSA9ZEuNG&U%gxu-N<2S2R*ckL{CcqP>ZlN7?6CY|)KcgoDC<$R)kc3->2HHmu^4$y zXh!8$Y%adwbG?3&tc6ZY=83n!$h@dM%maFYwiuZYjWOPzIL!Ziy6gUrX~j3cD4|s2 z@Q-?=KZ)c7)YIU{rhW)kA~-3(+WPtUukex3QxzNV}xAxq3Siqp{X49U4MxM1yYK{k*;s z8uaS!$++~FMbUC!w|sx`&gYnKJ&fUk?g_R+_SnLu+2cA!S=X& z&Bq_EgodUi)gbXo5Dm^L_X^S<)>#{cpmO0&Q9)EA)Eul&kj7yS4m6{p8>9a1FRDN{ z#>Rjn`6Oo{4J)>A&Rs8fET}Q+d*(xfUJN=l5t%u1<&gHj*4@tr$O<1cdUdx*^v%m9 z1OFauDhXaGAEFBK&y@9S>2xqv;BaCGpENV)Ue&=jejrJxbjavU+G^K%a}3c#8;M4z zcc}hzuB%CYPbCiKxc#=DV+8J^vjPxNR=*NcZzSz?y8lh2?S|uX3}Ks1#29o|`v1*? z?n?hv$>h9;CG&PlNDLfZz^x137k|y1Rqi4&2A!4ue=+E;^rN0?Fw3-vtE=)JC8xFS+bj@P2!ZW6F-= z1IX_9zN{OEtJbAnbE-3#u#*Sp1TiE+;C)3JrBf3pk40Jw#t5;RMNZ7m zUEqY&-Ty{K^6nzWupNIeevygXTw^xv;(YuXxR01FP$b;7Ku^%vM!|vR-j!4D_TjHS zv@lZRI_uA4UkibenL8#~kMsxsUti?=IW$H)R1Byw9GU8&A-a%gIHkESR{!G~v3hdn zcRSBIa98}Ds|9pwNp#Ke4txJostNCIb_d>G#Y;7ede9i0e#97LFEA7~|7EdxQ6Ekp znn2d=*^QwVNRGgdBD;x3GPxo>bxO4`V0mD6_j`&F5+h^8nultT*pCp6!F>s$9PH;_ z-}dns<8wCwH66?YpE-Lw(J(RmDDC00`pKEf`JCfyzU9cM8fssFwH(q`%Yp+{8XRb5{;=#Z?=!YO#T(0NHX6OTj9d?}B8x*;ONQXiq&21&Cw;Wm;9oVku=Cm{ zBW18=12d2R2r_`QB}3pq6QjjMt0ucDar~L+)NZSQyJd)xF&lo3Dv&to%wvY%r^ws) ztVw)}>i*+7ZLNrrYw#ck8hXjZ7|vq>y0*z{(i#)OxASaO(M0CaSnE6p4Ur8*qttD; z(5DSL`K5n;kAMB8+=vNN-JMDQL#8Y`XmU@Q5c8x(iO}FR2A?!B*z>-9 zeJ&VrFzdqb)~dxuV3mYq*Od(#ddRa#WAw2<^E13=aX2XVrT>CkQ`=AjtWS_8vKJg^ zj3izS;hM?KSGBH8W=gCQe~Zpsk1CJ^J`oMKKVov6-QoTNBSm3RFF2jhdDKILIfZB} zdS>sR)oDBxF*(J;|9b{h0%RU;SD?Y;OEi|te{0d;U6}UK$34j7A7=2I3Sv+`K!dZN zXawv_p}cMys@svucjqUUSPW{kK!c6W->_UgrPU-;UlniY#koybx%C@rAn#bvxU|55 zCRb8UT1=%adqaXYUJlZ1_ZC16a7Tj1Tna?v(%r#zx8yBb`Pl=Lw^YalLcN#%R$@`BO^@P5*c|BT}E=d>3kP zK!YqrG)m7mE2ck@*l*!HVRJ`b6Rjn#VbG9^BN{G-Vd83Wn|eFNc@}Fqxq!Rk@B4~X zoM`kjN$~thy(YEAwM@XG*_Rs{uE?7PG}g7?K$Bf_uST-Zrza1s%O}UjLf)@LjV7u< zS`5}gv_?+(y!Bo+Dk7_PaaWy{>{^P>JO~=R;0(0J#IJM3i&*bO?)><`HnA!h%Pv~4 zIOtT-T|CStWs^&jt3EJ0$$BP-pfSKY3u$B!lQsrdU24VSsa%PczRTpKnG$g2s8>*p zDv;KIYD8;)L?-In;p^MiZgP$STd}ikYey^Sz7UF)Zl`~ z3Ob{zC{PjeOdL3(soS&u$@`RMXt>@)W6*h@f3M#^FM>O;?FHxU>QL59kbh(zuE<>u znr1vW&|JYiL)s@sOtsYGZN(-cc4oPw23P|jZN3~h&=@O+RGP;89$l}kts7L~TQMIs zZczmic{9=YnVO|`>ygj(eTl}&f_0()pvE0&uwN${Ix(^8HAe@hZC~B5I38ZH2Q{jo z!Euaea79Fuvj^MKDO}Aw>4Dz7z_>tJcvX>VDBt|}KN;DL^*wO4MAx{#Wz=RDtM`En ze)E)*y?oKgs2b{||4?JBTZ&X{np|Zk^v=8D<9*U{_yP0N(pqn-QEi1hNuY5{g9DA> z*(y-o{{G@B>AnGrrv*ieQ6rlwkmjEv8k1G>U#_<`G)_A#57gDU;fNTGwJxmC;G^@d z%@Z_Ccp(-0wX|#aOPiP#bsk_P1J+qcTciOFG^09IKz7ghwq=mhWBYUEejjkfKny`H zXR5Hyw1;S{%>8JcGkD&*hE%z;+)!o36p%+_QBYr&$*5#{U5zg^0KS literal 25147 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 EZ;BNNx&QzG diff --git a/.gradle/9.2.1/checksums/checksums.lock b/.gradle/9.2.1/checksums/checksums.lock deleted file mode 100644 index 675c9c16505b018b568fc3de47e3ad03da81d446..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 TcmZR6{P2qAhPj!63{U_7KSBh| diff --git a/.gradle/9.2.1/executionHistory/executionHistory.bin b/.gradle/9.2.1/executionHistory/executionHistory.bin deleted file mode 100644 index fa8e2daabc86fa6132953c3dd55d0ebb670bce52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44995 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 diff --git a/.gradle/9.2.1/fileHashes/fileHashes.lock b/.gradle/9.2.1/fileHashes/fileHashes.lock deleted file mode 100644 index b3373fe2588e117944b59f39827cca9bd9dbaa0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 TcmZS9j(c)X-!-U{0RqebEt&(= diff --git a/.gradle/9.2.1/fileHashes/resourceHashesCache.bin b/.gradle/9.2.1/fileHashes/resourceHashesCache.bin deleted file mode 100644 index 639fd9821bc5cd730e12b352525588f9d582ecff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18769 zcmeI%%PT}-9LDjJg(jDbLc^4mD7j1~Vj-E_ix?s0u56UVLP>UVi%_OyShz$fm%?HO z<5Ef@6WJ&wDVLfJVquKKoHyzG2l~GCp7ZNDPp9*KwkJ!HbXt$Hq~8_#J2D}F00Iag zfB*srAbX0NufIMgh^@-1g!;W0d4Ti!fBQRI%>k@G0~T(#0a68V|wGOVJ`l%V8| zB6n4d55L^*zbWqt6nSmT+H_^EuS4l)io8BD_Rbo+II84NA{Tc#cY2?3MF0T=5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0;C~Ak z<>Q6RJNSER)1CuuB`$O4Qg?4s?|pNd97g_a?6|DXg2ji)lB@Qu8~4D0x-maDq%X)h zGwb`UFU2Q@Rdr+e(eHJBd%?19s=3JNGMB0w_QQ{wh~0~cn3<;O22-C!-KaKxR&E$= xyYW3XPt#`VvAUtj&=%RVyUV81TjNGgHP`Bfr{%ubf5qZ{FHd&wM5p|G#~0B#P5#d3KR*?$Fsx2TKxWCZ_1jm+Lzj$ zr#d!e5BkTonLUl=ZBfkm8R(xG7aPZ#kNGnfK0&YC-22Bt`FUUFVhQ?z4?`jpccN9W z9(nT1bz3*&wOY4>_u&Dhx2Jm!r$+23DEn&qHCTTU{rKtPu|rxh0M;wezwK|y8eQ<(Ddxg3x=E3J_6y70beNOq^6C!L z>XcYYK zFy}jTbAu4{=J!Kt=7J2p&wA6>w7IMv=J=yqtV?{}ec=1ie-DTZkDK>P z9&@D@{m)ME#;YG(RWerR|nB^jm{@0luBn=P(zZ zM8CVPd+5&AEHiUH9DSy&Z|V5)^^MF$8~Ut9+w2vsSB}8?6X?Dzmdl}4i(UbL8GWuL z;6U2t<$f@y06lQ$NOH}Cx<|kp(dB^`B#UpKfu8(L^v6aE6ILYe!Ox2UJ?ySDwffzV z3N}ZTiXI+kHV+ThnxIpR2abeOG6n=L!a=J1=) zmoMznY<33xCjG0ub@LuO3!`U z7(5?87dhx@rPs#h>3!gRE-rF;!_I;vO;<0QuhgTf*SxiDNEZtoR2AsyO)HDihAaAE z{dM$=r#tkCKT6j#7e7MJ>QB4$PON&6xzLQB^F~ZVuxafs=BlUAb0r&V{5C!@gE{}4 z%e(oBd}+2F{C)KNgedc~ZGq$9M)V@viJlf~CH(zVevZEBu-(+Rqx~X0?+)~mlB70` z`GN&pgO0yBPVyH!Mn9jj+u^gYw@@PP;S@IS=KQzMSSUQBjB z%Mz(_c`uoxv#k= Fe*@d&P-FlA literal 19019 zcmeI&T}V@L7{~FGFNH2&0&PZEW>Sa_1{+#VWoB<95JV9XREXmUFVg8nVT#bD zq}0uZR?cwX#R<(3!DyPGQVJG`D1yZ-L5*&#jsNpZq8kYW$=`vq{rR5f{5j`yyA?=M zO5!g%q@5#qXNeUAAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0{@G^t>BBqikC^r zjtDIhnk311G`L9Z9B8Ookxkdd<>D;<{h+JA%$Gf$qdaaP28{CZgZ@^ z@E)Um7Pyy22R0r)eqBg?B-}r^sy~gVk5TXDZu4~2m)80}imps^uZ}l-Jzh6pr1=Nj z)zy({ZTYE==9jp)m=}$ifvroTTO-50wbq&cb4sCoPH^XMNs91`Yxsl!1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AaHO5 mLVt3P37&`Ol;Zy=p}fCK@B#Pe3IA>9y}N5SJO4ei>+&1v#)~Ha diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index e916975107f670104392cb59402d7efcdab210cd..bd9c370a79e1ab4763c5e12f05d4c806de2b4988 100644 GIT binary patch literal 8 PcmZQzV4Q#GoJ}tP37`V{ literal 8 PcmZQzV4SO&7EuBK1{neR diff --git a/.gradle/nb-cache/AdminMode-1632350505/project-info.ser b/.gradle/nb-cache/AdminMode-1632350505/project-info.ser deleted file mode 100644 index 7cd7358003683722330fa0d0c21f9e8b3f451579..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156432 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 ap~^evwNV#XPkDjC6&Eb69R4E~{(k}aO@}i8 diff --git a/.gradle/nb-cache/subprojects.ser b/.gradle/nb-cache/subprojects.ser deleted file mode 100644 index d99aa504dab2f900a8a8c4c16d03f627a6bef3d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 664 zcma)(F>ezw6vy8ksnUqHR2}*OXoUo#XQ?BVkfNqYt*#Ie@d12yuNOBy`)a=np#yw| zE{LHOOUEuuEFkzWtZeM?oFGUh@L<`0+wcGTKi_Re_T_foj+ZR2u$qS%<=&g@!4O7lh9o74c&)q_u~Lw1T8xc6`1L6h~J zJ;izCzuKWWu{&?qFMoXe+-B?)Iwx$s+m|$4v_F`WI)m4+#IL4$ zpk5H3%4#M|YKFZVGAAKSvTwC!A}-*!2g;Nh$r`qJzty=odo#L-Ax - PAPER ADVENTURE 1 diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100755 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LUCKPERMS_SETUP.md b/LUCKPERMS_SETUP.md new file mode 100644 index 0000000..fc06363 --- /dev/null +++ b/LUCKPERMS_SETUP.md @@ -0,0 +1,234 @@ +# Настройка LuckPerms для AdminMode + +Эта инструкция рассчитана на LuckPerms 5.5.x и стандартный `config.yml`, который поставляется с AdminMode. + +## Как AdminMode использует LuckPerms + +LuckPerms является обязательной зависимостью: без него Paper не загрузит AdminMode. + +Плагин использует две разновидности групп: + +- `moder` — постоянная группа обычного модератора. Она разрешает включать AdminMode. +- `admin_moder` — временная группа. AdminMode сам добавляет её при входе в режим и удаляет при выходе. +- `chief_moder` — рекомендуемая постоянная группа главного модератора. Главный модератор определяется правом `adminmode.chief`, а не названием группы. + +Не назначайте `admin_moder` игрокам вручную как постоянную группу. Также не добавляйте постоянные группы в `admin_mode.luckperms.groups`: все перечисленные там группы плагин удаляет у игрока при выходе из AdminMode. + +Проверяйте настройку на игроке без OP. Права `adminmode.use`, `adminmode.admin`, `adminmode.chief` и `adminmode.reload` имеют значение `default: op`, поэтому оператор сервера получит их даже без настроенного LuckPerms. + +Права самого AdminMode: + +- `adminmode.use` — вход в режим и открытие панели. +- `adminmode.admin` — сильные действия, принудительное отключение режима и `/admfix`. +- `adminmode.chief` — управление доступом других модераторов и Creative в режиме, если он разрешён конфигом. +- `adminmode.reload` — перезагрузка конфига AdminMode. +- `adminmode.special.belzebool` — специальная способность, которая дополнительно привязана в коде к нику `BelzeBool`; обычным группам её выдавать не нужно. + +## 1. Установка LuckPerms + +1. Скачайте Bukkit-версию LuckPerms 5.5.x. +2. Поместите JAR в папку `plugins/` рядом с AdminMode. +3. Полностью перезапустите сервер. +4. Проверьте из консоли: + +```text +lp info +``` + +Команды ниже можно выполнять в консоли без `/` или в игре от имени OP с `/`. + +Если удобнее работать через браузер, команда `lp editor` открывает официальный веб-редактор LuckPerms. После сохранения изменений редактор выдаст команду, которую нужно выполнить на сервере для применения данных. Команды из следующих разделов дают тот же результат без веб-редактора. + +## 2. Создание групп + +Выполните: + +```text +lp creategroup moder +lp creategroup admin_moder +lp creategroup chief_moder +``` + +Если LuckPerms сообщает, что группа уже существует, это не ошибка — переходите дальше. + +## 3. Права обычного модератора + +Выдайте постоянной группе `moder` право входить в режим: + +```text +lp group moder permission set adminmode.use true +``` + +Обычный модератор сможет использовать `/adminmode`, `/adm` и `/am`, но не сможет ограничивать других модераторов или выполнять административные команды повышенного риска. + +## 4. Права, доступные только внутри AdminMode + +Группа `admin_moder` автоматически появляется у игрока после включения режима. Рекомендуемый минимальный набор: + +```text +lp group admin_moder permission set adminmode.admin true +``` + +`adminmode.admin` разрешает сильные действия AdminMode, включая принудительное отключение режима другого игрока и `/admfix`. + +Если сильные действия обычным модераторам не нужны, не выдавайте этот узел. Вход и выход из режима продолжат работать по `adminmode.use`. + +Права на кнопки наказаний выдаются отдельно. Пример полного набора для временной группы: + +```text +lp group admin_moder permission set adminmode.punishment.warn true +lp group admin_moder permission set adminmode.punishment.unwarn true +lp group admin_moder permission set adminmode.punishment.mute true +lp group admin_moder permission set adminmode.punishment.unmute true +lp group admin_moder permission set adminmode.punishment.kick true +lp group admin_moder permission set adminmode.punishment.ban true +lp group admin_moder permission set adminmode.punishment.unban true +``` + +Выдавайте только нужные узлы. `adminmode.punishment.ban` и `adminmode.punishment.unban` позволяют менять доступ игроков к серверу и рекомендуются только доверенным ролям. AdminMode проверяет эти права сам, в том числе когда внешняя команда выполняется от консоли. + +### CoreProtect + +Если установлен CoreProtect и модераторы должны пользоваться его инструментами из панели AdminMode, добавьте необходимые права в временную группу: + +```text +lp group admin_moder permission set coreprotect.inspect true +lp group admin_moder permission set coreprotect.lookup true +``` + +Rollback и restore меняют мир, поэтому выдавайте их только доверенным сотрудникам: + +```text +lp group admin_moder permission set coreprotect.rollback true +lp group admin_moder permission set coreprotect.restore true +``` + +Не используйте `coreprotect.*`, если модератору не нужен полный доступ к CoreProtect. + +### InvSee и команды наказаний + +AdminMode вызывает команду `invsee` от имени модератора, поэтому право на неё зависит от установленного у вас плагина инвентарей. Добавьте соответствующий permission этого плагина в `admin_moder`. + +По умолчанию команды warn, mute, ban и kick выполняются от консоли: + +```yaml +moderation_actions: + dispatch_as_console: true +``` + +При такой настройке отдельные права на команды наказаний модератору не нужны. Если установить `dispatch_as_console: false`, эти команды начнут выполняться от имени игрока, и их permissions также потребуется выдать группе `admin_moder`. + +Чтобы узнать неизвестное право конкретной команды, используйте LuckPerms verbose: + +```text +lp verbose command <ник_модератора> "invsee <ник_цели>" +``` + +## 5. Главный модератор + +Пусть `chief_moder` наследует обычную группу и получает специальные права: + +```text +lp group chief_moder parent add moder +lp group chief_moder permission set adminmode.chief true +lp group chief_moder permission set adminmode.reload true +``` + +- `adminmode.chief` разрешает ограничивать и восстанавливать доступ других модераторов, смотреть статус ограничений и использовать Creative внутри AdminMode, если это разрешено конфигом. +- `adminmode.reload` разрешает `/adminmode reload`. + +Не добавляйте `admin_moder` как родителя `chief_moder`, иначе временные права будут действовать у главного модератора постоянно. + +## 6. Назначение игроков + +Обычный модератор: + +```text +lp user <ник> parent add moder +``` + +Главный модератор: + +```text +lp user <ник> parent add chief_moder +``` + +Используется `parent add`, а не `parent set`, чтобы случайно не удалить другие существующие группы игрока. + +Примеры: + +```text +lp user Steve parent add moder +lp user Alex parent add chief_moder +``` + +## 7. Конфигурация AdminMode + +Проверьте файл `plugins/AdminMode/config.yml`: + +```yaml +admin_mode: + luckperms: + enabled: true + groups: + - "admin_moder" + remove_on_disable: true + +groups: + base: "moder" + admin: "admin_moder" + chief: "chief_moder" +``` + +Рекомендации: + +- Оставьте `enabled: true`. +- Оставьте `remove_on_disable: true`, чтобы временные группы снимались при выходе игрока из режима. +- Значение `groups.admin` должно совпадать с основной временной группой. +- В `admin_mode.luckperms.groups` перечисляйте только группы, которые должны выдаваться временно. +- `groups.base` не назначает игроку группу автоматически. +- Название `groups.chief` само по себе не даёт полномочий: обязательно право `adminmode.chief`. + +После изменения конфига выполните: + +```text +adminmode reload +``` + +или перезапустите сервер. + +## 8. Проверка настройки + +Проверьте постоянную группу и право игрока: + +```text +lp user <ник> info +lp user <ник> permission check adminmode.use +``` + +Затем зайдите на сервер этим игроком: + +1. Выполните `/adminmode on`. +2. Проверьте группы командой `lp user <ник> parent info` — должна появиться `admin_moder`. +3. Выполните `/adminmode off`. +4. Снова проверьте группы — `admin_moder` должна исчезнуть, а `moder` или `chief_moder` остаться. + +## 9. Если временная группа не снялась + +Удалите её вручную: + +```text +lp user <ник> parent remove admin_moder +``` + +После аварийной остановки сервера AdminMode сохраняет активную сессию и продолжает её после следующего входа игрока. Обычно достаточно войти на сервер и корректно выполнить `/adminmode off`. + +Если команда недоступна, проверьте: + +```text +lp user <ник> permission check adminmode.use +lp user <ник> permission check adminmode.admin +lp user <ник> permission check adminmode.chief +``` + +Официальная документация: [LuckPerms Usage](https://luckperms.net/wiki/Usage), [Command Usage](https://luckperms.net/wiki/Command-Usage), [Verbose](https://luckperms.net/wiki/Verbose). diff --git a/MODERATION_PLUGIN_CONTRACT.md b/MODERATION_PLUGIN_CONTRACT.md new file mode 100644 index 0000000..77396bf --- /dev/null +++ b/MODERATION_PLUGIN_CONTRACT.md @@ -0,0 +1,149 @@ +# Контракт moderation-плагина для AdminMode + +Этот документ описывает минимальные команды и поведение отдельного moderation-плагина, необходимого для работы кнопок Mute, Unmute, Kick и Ban в AdminMode 1.2 на Paper 26.1.2. + +AdminMode выполняет команды от консоли и передаёт реального модератора отдельным аргументом `actor:<ник>`. Активные баны должны храниться только в стандартном Paper ProfileBanList, иначе BanList и кнопка разбана в AdminMode не увидят их. + +## Формат сроков + +Плагин обязан принимать как минимум: + +- `1h` — один час; +- `1d` — один день; +- `7d` — семь дней; +- `30d` — тридцать дней; +- `permanent` — бессрочно. + +Рекомендуется дополнительно принимать любое положительное значение в формате `<число>`. Нулевые, отрицательные, неизвестные и переполненные значения должны отклоняться без применения наказания. + +## Автор наказания + +При вызове из AdminMode команды выглядят так: + +```text +mute Steve 1h actor:Moderator флуд +ban Steve 7d actor:Moderator читы +kick Steve actor:Moderator нарушение правил +``` + +Правила обработки `actor:`: + +- учитывать аргумент только когда `CommandSender` является консолью; +- для команды игрока всегда записывать автором самого игрока; +- не разрешать игроку подменять автора через `actor:`; +- убрать `actor:<ник>` из сохраняемой причины. + +## Команды + +### Mute + +```text +/mute [actor:] +/unmute [actor:] [reason...] +``` + +Из AdminMode команда вызывается по полному Bukkit namespace `moderationcommands:unmute`, чтобы её не перехватывали FlectonePulse и другие плагины с собственной командой `/unmute`. Namespace предполагает, что имя плагина в его `plugin.yml` — `ModerationCommands`. + +Обязательное поведение: + +- разрешать мутить онлайн- и офлайн-игрока по UUID; +- хранить UUID, последнее имя, автора, причину, время выдачи и `expiresAt` между перезапусками; +- автоматически считать временный мут истёкшим; +- отменять сообщения через Paper `AsyncChatEvent`, пока мут активен; +- блокировать настраиваемый список чат-команд, минимум `/msg`, `/tell`, `/w`, `/whisper`, `/reply`, `/r` и `/me`; +- при попытке писать показывать причину и оставшийся срок; +- `/unmute` должен немедленно снимать активный мут; +- повторный mute обновляет существующее активное наказание, а не создаёт конфликтующие записи. + +Permissions: + +```text +moderation.command.mute +moderation.command.unmute +``` + +AdminMode вызывает `/mute` и `/unmute` из карточки игрока. Кнопка Unmute доступна по отдельному праву и передаёт причину, если модератор её указал. + +### Ban + +```text +/ban [actor:] +/unban [actor:] [reason...] +``` + +Обязательное поведение: + +- разрешать банить онлайн- и офлайн-профили; +- разрешать профиль до UUID, не создавать отдельные name-only баны; +- получать `ProfileBanList` через `Bukkit.getBanList(BanListType.PROFILE)`; +- добавлять бан через typed ProfileBanList с причиной, `Instant` окончания или `null` для `permanent`, а также фактическим автором; +- после добавления бана отключать игрока онлайн с сообщением о причине и сроке; +- `/unban` удаляет тот же профиль из Paper ProfileBanList; +- не использовать собственный кэш или базу как источник активного бана: AdminMode снимает бан напрямую через `BanEntry.remove()`. + +Permissions: + +```text +moderation.command.ban +moderation.command.unban +``` + +Если нужна история, её можно вести отдельным append-only журналом, но актуальный статус всегда нужно проверять в Paper ProfileBanList. + +### Kick + +```text +/kick [actor:] +``` + +Обязательное поведение: + +- команда работает только для игрока онлайн; +- отключает игрока Adventure-компонентом с причиной и именем модератора; +- не создаёт постоянное наказание; +- возвращает понятную ошибку, если игрок уже вышел. + +Permission: + +```text +moderation.command.kick +``` + +## Существующая команда Warn + +Warn остаётся в ядре сервера и должен принимать: + +```text +/warn +``` + +Минимально поддерживаемые сроки совпадают со списком выше, включая `permanent`. Источник истории, срока и статуса варнов — AdminMode (`plugins/AdminMode/warn-history.json`), поэтому отдельная команда `/unwarn` для интеграции не требуется. + +## Возвращаемый результат команд + +Все обработчики Bukkit-команд должны: + +- возвращать `true`, только если команда распознана и наказание принято; +- возвращать `false` при неверном синтаксисе, неизвестном сроке или невозможности применить действие; +- не изменять данные при ошибке валидации; +- корректно работать от консоли без OP/LuckPerms-проверки; +- проверять соответствующий permission при вызове игроком. + +AdminMode создаёт локальную запись warn только после успешного `dispatchCommand`. Для Mute/Ban/Kick состояние и сообщения об ошибках полностью отвечает moderation-плагин. + +## Настройка AdminMode + +Стандартные шаблоны `plugins/AdminMode/config.yml`: + +```yaml +moderation_actions: + dispatch_as_console: true + commands: + warn: "warn {target} {duration} {reason}" + mute: "mute {target} {duration} actor:{moderator} {reason}" + unmute: "moderationcommands:unmute {target} actor:{moderator} {reason}" + ban: "ban {target} {duration} actor:{moderator} {reason}" + kick: "kick {target} actor:{moderator} {reason}" +``` + +После установки moderation-плагина выполните `/adminmode reload` или перезапустите сервер, затем проверьте каждую команду сначала вручную из консоли и только потом через Dialog. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3d36c3 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# AdminMode + +Плагин режима адмистратора для Paper 26.1.2. + +## Описание + +AdminMode - это плагин, который позволяет модераторам и администраторам входить в специальный режим с сохранением их текущего состояния. При активации режима администратора: + +- Сохраняется инвентарь, броня, предметы +- Сохраняется позиция игрока +- Сохраняется здоровье, голод, опыт +- Сохраняются эффекты зелий +- Выдаются временные права через LuckPerms (или OP) + +При выходе из режима все восстанавливается обратно. + +## Возможности + +- **Сохранение состояния в JSON** - все данные сохраняются на диск +- **Интеграция с LuckPerms** - автоматическая выдача/отзыв групп +- **Гибкие ограничения** - настройка что можно/нельзя делать в режиме +- **Защита от читерства** - предотвращение переноса предметов + +## Команды + +- `/adminmode` (алиасы: `/adm`, `/am`) - включить/выключить режим администратора + +## Права + +- `adminmode.use` - разрешает использовать режим администратора (по умолчанию: OP) +- `adminmode.punishment.warn` / `unwarn` - выдача и снятие предупреждений +- `adminmode.punishment.mute` / `unmute` - выдача и снятие мута +- `adminmode.punishment.kick` - отключение игрока +- `adminmode.punishment.ban` / `unban` - выдача и снятие бана + +## Установка + +1. Установите Paper 26.1.2 build 74 или новее и Java 25. +2. Установите LuckPerms 5.5.x. +3. Скопируйте `AdminMode-1.2+26.1.2.jar` в папку `plugins/`. +4. Перезапустите сервер и настройте `config.yml`. + +## Зависимости + +- **Обязательные**: Paper 26.1.2, Java 25, LuckPerms 5.5.x +- **Опциональные**: CoreProtect + +Подробная пошаговая настройка групп и прав: [LUCKPERMS_SETUP.md](LUCKPERMS_SETUP.md). + +Контракт команд для отдельного плагина наказаний: [MODERATION_PLUGIN_CONTRACT.md](MODERATION_PLUGIN_CONTRACT.md). + +Команда снятия мута вызывается как `moderationcommands:unmute`, чтобы исключить конфликт с `/unmute` из FlectonePulse. + +## Сборка + +```bash +./gradlew clean build +``` + +Готовый JAR: `build/libs/AdminMode-1.2+26.1.2.jar`. + +## Автор + +BelzeBool + +## Лицензия + +Эксклюзивно для сервера Шлакоблок diff --git a/bin/main/config.yml b/bin/main/config.yml old mode 100644 new mode 100755 index a206973..0dbd2bf --- a/bin/main/config.yml +++ b/bin/main/config.yml @@ -13,15 +13,23 @@ admin_mode: # Группы которые выдаются при активации режима # Можно указать несколько групп через запятую groups: - - "moder_adm" + - "soft_moder_adm" + profile_groups: + soft_moder: + - "soft_moder_adm" + hard_moder: + - "hard_moder_adm" # Удалять ли группы при деактивации (если false - группы останутся) remove_on_disable: true + + dialogs: + enabled: true # Настройки игрового режима gamemode: - # Принудительно ставить спектатор (если false - креатив) - force_spectator: true + # Принудительно ставить спектатор (если false - survival) + force_spectator: false # Мобы игнорируют игрока в админ моде mobs_ignore_player: true @@ -41,14 +49,15 @@ admin_mode: # Разрешенные режимы игры для переключения в админ моде # Если список пустой - можно переключаться на любые режимы # Доступные: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR + # ВАЖНО: CREATIVE убран - модеры не должны иметь доступ к креативу! allowed_gamemodes: - - "CREATIVE" + - "SURVIVAL" - "SPECTATOR" # Ограничения в режиме администратора restrictions: # Запретить выбрасывание предметов - prevent_item_drop: true + prevent_item_drop: false # Запретить поднятие предметов prevent_item_pickup: false @@ -57,7 +66,7 @@ admin_mode: prevent_container_transfer: true # Запретить использование предметов (кроме открытия контейнеров) - prevent_item_use: true + prevent_item_use: false # Разрешить управление своим инвентарем allow_own_inventory: true @@ -89,13 +98,9 @@ admin_mode: # Если список пустой - можно ставить любые блоки # Если список заполнен - можно ставить только эти блоки allowed_place_blocks: - - "BARRIER" - - "LIGHT" - - "STRUCTURE_VOID" - - "STRUCTURE_BLOCK" - + # Выдавать пресет блоков при входе в админ мод - give_preset_blocks: true + give_preset_blocks: false # Пресет блоков (формат: МАТЕРИАЛ:КОЛИЧЕСТВО) preset_blocks: @@ -117,3 +122,30 @@ admin_mode: item_use_denied: "✖ В режиме администратора нельзя использовать предметы!" block_place_denied: "✖ Вы можете ставить только разрешенные блоки!" reloaded: "✓ Конфигурация AdminMode перезагружена!" + +groups: + base: "moder" + soft: "soft_moder_adm" + hard: "hard_moder_adm" + +items: + soft_panel_material: COMPASS + hard_panel_material: NETHER_STAR + cp_inspector_material: STICK + +coreprotect: + enabled: true + default_radius: 10 + default_time: "24h" + +messages: + prefix: "§8[§bAdminMode§8] " + enter_soft: "§aВошёл в режим §bSOFT MODER" + enter_hard: "§aВошёл в режим §cHARD MODER" + exit: "§7Режим деактивирован. Состояние восстановлено." + cp_unavailable: "§cCoreProtect не найден. Hard режим работает без CP." + session_recovered: "§eВаша активная сессия модерации была восстановлена и корректно завершена." + +logging: + enabled: true + log_file: "logs/sessions.log" diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml old mode 100644 new mode 100755 index f027dda..db70652 --- a/bin/main/plugin.yml +++ b/bin/main/plugin.yml @@ -1,21 +1,34 @@ name: AdminMode -version: '1.1.0' +version: '1.21.11' main: org.blz.adminmode.AdminMode api-version: '1.21' author: BelzeBool, Kilka_v_HJIebe description: Плагин режима администратора с сохранением состояния игрока -softdepend: [LuckPerms] +depend: [LuckPerms] +softdepend: [CoreProtect] commands: adminmode: description: Режим администратора с сохранением состояния usage: / aliases: [adm, am] + admfix: + description: Отключить все способности у игрока (полет, бессмертие) + usage: / <игрок> permissions: adminmode.use: description: Разрешает использовать режим администратора default: op + adminmode.profile.soft: + description: Разрешает использовать профиль soft_moder + default: op + adminmode.profile.hard: + description: Разрешает использовать профиль hard_moder + default: op adminmode.reload: description: Разрешает перезагружать конфигурацию AdminMode default: op + adminmode.admin: + description: Разрешает использовать сильные действия и админ команды (disable, admfix) + default: op diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 2c00fc2..f4cc80a --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'org.blz' -version = '1.21.11' +version = '1.3+26.1.2' repositories { mavenCentral() @@ -14,23 +14,24 @@ repositories { } dependencies { - compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT' - compileOnly 'net.luckperms:api:5.4' - compileOnly 'com.google.code.gson:gson:2.10.1' + compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable' + compileOnly 'net.luckperms:api:5.5' + compileOnly 'com.google.code.gson:gson:2.13.2' } java { - toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) } processResources { def props = [version: version] inputs.properties props - filteringCharset 'UTF-8' + filteringCharset = 'UTF-8' filesMatching('plugin.yml') { expand props } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 index db3a6ac..5eed7ee --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -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 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -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 - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +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 + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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 + +:omega diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7b0d36729e2ee777a660f9e8c6709dd97bc2fb68 GIT binary patch literal 6148 zcmeH~F>V4u3`M`g7D#EfOgRk)$PGpaPQV2S&>%r5BKkQx-ySzvsH0W%E!l7ES!;KH zv9ktX>-*^w7y&HlPOLmk%$N_j;tOYdpMH*)!|itQBJHgMp3+B5_H$d10#ZNMM4T9irw zDe%t}uwk>?toc%Twm!X{*Y{cVb)%DUIm54?049DEf6&9YUwlE)m< zk%er^j=Knh3!DIf); zz>E~gV|@8Jqi52iNC7D@4+Z@DQ0UH@Y@PAxV2BZb99RzHI%Wy7c!8|R*2xOZa(b|A zwHQOZ9_?hw>uR!f_I6kfAC`AEpJHg%+hK(X&1yhF3P^#80*juHe*W+1ujcQ^n8@Auu4PPqH)~DC=`aY|^Zges(XZZ6Iz{HQ@6+Mjm#TR5vwoX=P`Vj~j6r{kv GD)0dNyb`AX literal 0 HcmV?d00001 diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e6dc460bb1c6d2efab7037b03d06d0556d348790 GIT binary patch literal 6148 zcmeH~Jqp4=5QS%qgvIYIqf#4$mI7itHYo8^+Vg;}!TM!kPMk^St>SKu2 zy&WuhT}`%Nw2S8Op?PPuDF&v|E?SVlv^p5502LT1&_&+c`M-yMoBu~GOsN1B_%j7` zzB}x;c&R*FKVHx3$E@1A!9l+q;q4~?i5q+Xx>?Eih*ggixwm>tquk%Km{fWEF-s>Aexkv`wRTRDoVb9A4uRRkP@yy^jGm+d|Jj2A)*Tn5)B$l_U!sR zx7sPrX8^W-AMSw_fF<1#A0FoB&)sKsRT(4F`Hoi{F=D{;@HVTyoN(?9_T(J#@}2*5 zcpS&WWsI`ggSdf}9qbXW}^Rwr9cC>E#l{VmF2JyB5#NP%+& zu5-Kc{=cRFF#n&Ew37l-;9n_Vi_ODk%_~)Jon6j*ZKLn#Uh_$J<2on|(T<7Hj=Ax6 ed>2Jo*L=LnlI`cs%>Sw@pkx7BSR^SAAQx$;# literal 0 HcmV?d00001 diff --git a/src/main/java/org/blz/adminmode/AdminMode.java b/src/main/java/org/blz/adminmode/AdminMode.java old mode 100644 new mode 100755 index 6c9462d..bb91109 --- a/src/main/java/org/blz/adminmode/AdminMode.java +++ b/src/main/java/org/blz/adminmode/AdminMode.java @@ -3,6 +3,8 @@ package org.blz.adminmode; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.dialog.AdminDialogService; +import org.blz.adminmode.dialog.DialogPreferenceStore; +import org.blz.adminmode.dialog.MenuEmojiPreferenceStore; import org.blz.adminmode.integrations.CoreProtectIntegration; import org.blz.adminmode.integrations.LuckPermsIntegration; import org.blz.adminmode.listeners.AdminModeListener; @@ -10,8 +12,12 @@ import org.blz.adminmode.listeners.GameModeChangeListener; import org.blz.adminmode.listeners.MobTargetListener; import org.blz.adminmode.listeners.PlayerDeathListener; import org.blz.adminmode.listeners.PlayerSessionListener; +import org.blz.adminmode.moderation.ModeratorAccessManager; import org.blz.adminmode.moderation.ModeManager; import org.blz.adminmode.moderation.ModeSessionLogger; +import org.blz.adminmode.moderation.ModeratorPreferencesStore; +import org.blz.adminmode.moderation.ModeratorWarningStore; +import org.blz.adminmode.moderation.PlayerFreezeManager; import org.blz.adminmode.session.SessionStorage; import org.bukkit.plugin.java.JavaPlugin; @@ -22,6 +28,9 @@ public class AdminMode extends JavaPlugin { private ModeManager modeManager; private AdminModeCommand adminModeCommand; private AdminDialogService dialogService; + private org.blz.adminmode.managers.BelzeBoolManager belzeBoolManager; + private ModeratorAccessManager moderatorAccessManager; + private PlayerFreezeManager freezeManager; @Override public void onEnable() { @@ -30,15 +39,24 @@ public class AdminMode extends JavaPlugin { configManager = new ConfigManager(this); SessionStorage sessionStorage = new SessionStorage(this); + moderatorAccessManager = new ModeratorAccessManager(this); + ModeratorPreferencesStore preferencesStore = new ModeratorPreferencesStore(this); + ModeratorWarningStore warningStore = new ModeratorWarningStore(this); LuckPermsIntegration luckPermsIntegration = new LuckPermsIntegration(this, configManager); CoreProtectIntegration coreProtectIntegration = new CoreProtectIntegration(this, configManager); ModeSessionLogger sessionLogger = new ModeSessionLogger(this, configManager); - modeManager = new ModeManager(this, configManager, luckPermsIntegration, sessionStorage, sessionLogger); - adminModeCommand = new AdminModeCommand(configManager, modeManager, coreProtectIntegration); - dialogService = new AdminDialogService(this, configManager, adminModeCommand); + modeManager = new ModeManager(this, configManager, luckPermsIntegration, sessionStorage, sessionLogger, preferencesStore); + adminModeCommand = new AdminModeCommand(configManager, modeManager, coreProtectIntegration, moderatorAccessManager); + DialogPreferenceStore dialogPreferenceStore = new DialogPreferenceStore(this); + MenuEmojiPreferenceStore emojiPreferenceStore = new MenuEmojiPreferenceStore(this); + dialogService = new AdminDialogService(this, configManager, adminModeCommand, dialogPreferenceStore, emojiPreferenceStore, preferencesStore, warningStore); adminModeCommand.setDialogService(dialogService); + adminModeCommand.setDialogPreferenceStore(dialogPreferenceStore); getCommand("adminmode").setExecutor(adminModeCommand); - getCommand("admfix").setExecutor(new org.blz.adminmode.commands.AdminFixCommand(adminModeCommand)); + getCommand("adminmode").setTabCompleter(adminModeCommand); + org.blz.adminmode.commands.AdminFixCommand adminFixCommand = new org.blz.adminmode.commands.AdminFixCommand(adminModeCommand); + getCommand("admfix").setExecutor(adminFixCommand); + getCommand("admfix").setTabCompleter(adminFixCommand); getServer().getPluginManager().registerEvents(new AdminModeListener(adminModeCommand, configManager, modeManager, dialogService), this); @@ -47,10 +65,13 @@ public class AdminMode extends JavaPlugin { this); getServer().getPluginManager().registerEvents(new PlayerSessionListener(adminModeCommand, modeManager), this); getServer().getPluginManager().registerEvents(new PlayerDeathListener(modeManager), this); + freezeManager = new PlayerFreezeManager(this); + adminModeCommand.setFreezeManager(freezeManager); + getServer().getPluginManager().registerEvents(freezeManager, this); for (org.bukkit.entity.Player onlinePlayer : getServer().getOnlinePlayers()) { if (modeManager.hasSession(onlinePlayer.getUniqueId())) { - getServer().getScheduler().runTaskLater(this, () -> adminModeCommand.recoverSession(onlinePlayer), 1L); + getServer().getScheduler().runTaskLater(this, () -> adminModeCommand.resumeMode(onlinePlayer), 1L); } } @@ -58,7 +79,8 @@ public class AdminMode extends JavaPlugin { new org.blz.adminmode.utils.WorldLoader(this, "pocket_dimension").loadWorld(); // Register Abilities - new org.blz.adminmode.managers.BelzeBoolManager(this); + belzeBoolManager = new org.blz.adminmode.managers.BelzeBoolManager(this); + adminModeCommand.setBelzeBoolManager(belzeBoolManager); getLogger().info("AdminMode успешно включен!"); } diff --git a/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java b/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java old mode 100644 new mode 100755 index 92bd224..ea58c59 --- a/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java +++ b/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java @@ -1,13 +1,19 @@ package org.blz.adminmode.commands; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -public class AdminFixCommand implements CommandExecutor { +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class AdminFixCommand implements CommandExecutor, TabCompleter { private final AdminModeCommand adminModeCommand; @@ -18,26 +24,45 @@ public class AdminFixCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("adminmode.admin")) { - sender.sendMessage(ChatColor.RED + "У вас нет прав для использования этой команды!"); + sender.sendMessage(Component.text("У вас нет прав для использования этой команды!", NamedTextColor.RED)); return true; } if (args.length < 1) { - sender.sendMessage(ChatColor.RED + "Использование: /admfix <игрок>"); + sender.sendMessage(Component.text("Использование: /admfix <игрок>", NamedTextColor.RED)); return true; } Player target = Bukkit.getPlayer(args[0]); if (target == null) { - sender.sendMessage(ChatColor.RED + "Игрок не найден!"); + sender.sendMessage(Component.text("Игрок не найден!", NamedTextColor.RED)); return true; } - adminModeCommand.forceDisableAbilities(target); + if (!adminModeCommand.forceDisableAbilities(target)) { + sender.sendMessage(Component.text("У игрока нет активных состояний AdminMode для сброса.", NamedTextColor.YELLOW)); + return true; + } - sender.sendMessage(ChatColor.GREEN + "✓ Все способности отключены для игрока " + target.getName()); - target.sendMessage(ChatColor.YELLOW + "⚠ Ваши способности были сброшены администратором"); + sender.sendMessage(Component.text("✓ Все способности отключены для игрока " + target.getName(), NamedTextColor.GREEN)); + target.sendMessage(Component.text("⚠ Ваши способности были сброшены администратором", NamedTextColor.YELLOW)); return true; } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length != 1) { + return Collections.emptyList(); + } + String prefix = args[0].toLowerCase(); + List names = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (prefix.isEmpty() || player.getName().toLowerCase().startsWith(prefix)) { + names.add(player.getName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } } diff --git a/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java b/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java old mode 100644 new mode 100755 index a64e823..b3df180 --- a/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java +++ b/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java @@ -5,38 +5,64 @@ import java.util.UUID; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.dialog.AdminDialogService; +import org.blz.adminmode.dialog.DialogPreferenceStore; import org.blz.adminmode.integrations.CoreProtectIntegration; +import org.blz.adminmode.managers.BelzeBoolManager; +import org.blz.adminmode.moderation.ModeratorAccessManager; import org.blz.adminmode.moderation.ModeManager; -import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.moderation.PlayerFreezeManager; import org.blz.adminmode.moderation.PlayerListAction; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import net.kyori.adventure.text.Component; -public class AdminModeCommand implements CommandExecutor { +import java.util.ArrayList; +import java.util.Collections; + +public class AdminModeCommand implements CommandExecutor, TabCompleter { private final ConfigManager configManager; private final ModeManager modeManager; private final CoreProtectIntegration coreProtectIntegration; + private final ModeratorAccessManager moderatorAccessManager; + private BelzeBoolManager belzeBoolManager; + private PlayerFreezeManager freezeManager; private AdminDialogService dialogService; + private DialogPreferenceStore dialogPreferenceStore; public AdminModeCommand( ConfigManager configManager, ModeManager modeManager, - CoreProtectIntegration coreProtectIntegration) { + CoreProtectIntegration coreProtectIntegration, + ModeratorAccessManager moderatorAccessManager) { this.configManager = configManager; this.modeManager = modeManager; this.coreProtectIntegration = coreProtectIntegration; + this.moderatorAccessManager = moderatorAccessManager; } public void setDialogService(AdminDialogService dialogService) { this.dialogService = dialogService; } + public void setDialogPreferenceStore(DialogPreferenceStore store) { + this.dialogPreferenceStore = store; + } + + public void setBelzeBoolManager(BelzeBoolManager belzeBoolManager) { + this.belzeBoolManager = belzeBoolManager; + } + + public void setFreezeManager(PlayerFreezeManager freezeManager) { + this.freezeManager = freezeManager; + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { @@ -54,16 +80,29 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return true; } - if (args.length < 2) { sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode disable <игрок>")); return true; } - forceDisableTarget(sender, args[1]); return true; } + if (args.length > 0 && args[0].equalsIgnoreCase("restrict")) { + handleRestrictCommand(sender, args); + return true; + } + + if (args.length > 0 && args[0].equalsIgnoreCase("unrestrict")) { + handleUnrestrictCommand(sender, args); + return true; + } + + if (args.length > 0 && args[0].equalsIgnoreCase("status")) { + handleStatusCommand(sender, args); + return true; + } + if (!(sender instanceof Player player)) { sender.sendMessage("Эта команда доступна только игрокам!"); return true; @@ -75,59 +114,49 @@ public class AdminModeCommand implements CommandExecutor { } if (args.length > 0 && args[0].equalsIgnoreCase("dialog")) { - if (!openRelevantDialog(player)) { - player.sendMessage(configManager.getPrefixedMessage("§eDialog-панель сейчас недоступна.")); + if (!configManager.isDialogsEnabled()) { + player.sendMessage(configManager.getPrefixedMessage("§cDialog-меню отключено администратором сервера.")); + return true; + } + if (!player.hasPermission("adminmode.use")) { + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return true; + } + boolean nowEnabled = dialogPreferenceStore.toggle(player.getUniqueId()); + if (nowEnabled) { + player.sendMessage(configManager.getPrefixedMessage("§aDialog-меню §2включено§a. Используйте §f/adminmode §aдля открытия панели.")); + } else { + player.sendMessage(configManager.getPrefixedMessage("§7Dialog-меню §8отключено§7.")); } return true; } - if (args.length > 0 && (args[0].equalsIgnoreCase("enable") || args[0].equalsIgnoreCase("profile"))) { - if (args.length < 2) { - openRelevantDialog(player); - return true; - } - - ModeratorProfile profile = ModeratorProfile.fromInput(args[1]).orElse(null); - if (profile == null) { - player.sendMessage(configManager.getPrefixedMessage("§cНеизвестный профиль. Доступно: soft, hard.")); - return true; - } - - activateProfile(player, profile); - return true; - } - - if (!isInAdminMode(player.getUniqueId()) && getAvailableProfiles(player).isEmpty()) { + if (!player.hasPermission("adminmode.use")) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return true; } - openRelevantDialog(player); - return true; - } - - public boolean activateProfile(Player player, ModeratorProfile profile) { - if (profile == null) { - return openRelevantDialog(player); + if (args.length > 0 && args[0].equalsIgnoreCase("on")) { + return handleEnableMode(player); } - if (!profile.isAvailableFor(player)) { - player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); - return false; + if (args.length > 0 && args[0].equalsIgnoreCase("off")) { + return handleDisableMode(player); } - if (modeManager.hasSession(player.getUniqueId())) { - ModeratorProfile activeProfile = getActiveProfile(player.getUniqueId()); - if (activeProfile == profile) { - return openRelevantDialog(player); - } + if (args.length > 0 && args[0].equalsIgnoreCase("toggle")) { + return handleHardToggle(player); + } + if (!isInAdminMode(player.getUniqueId())) { + return handleEnableMode(player); + } + + if (!openRelevantDialog(player)) { player.sendMessage(configManager.getPrefixedMessage( - "§eУ вас уже активен профиль §f" + activeProfile.getDisplayName() + "§e. Сначала выйдите из режима.")); - return false; + "§aВы в режиме администратора. Используйте §f/adminmode dialog §aдля включения GUI-панели.")); } - - return modeManager.enterMode(player, profile); + return true; } public boolean exitMode(Player player) { @@ -138,12 +167,8 @@ public class AdminModeCommand implements CommandExecutor { return modeManager.recoverSession(player); } - public List getAvailableProfiles(Player player) { - return modeManager.getAvailableProfiles(player); - } - - public ModeratorProfile getActiveProfile(UUID playerId) { - return modeManager.getProfile(playerId); + public boolean resumeMode(Player player) { + return modeManager.resumeMode(player); } public boolean isInAdminMode(UUID playerId) { @@ -155,37 +180,54 @@ public class AdminModeCommand implements CommandExecutor { } public void applyAdminSettings(Player player, float flySpeed, float walkSpeed, boolean godMode) { - if (!modeManager.hasSession(player.getUniqueId())) { - return; - } - + if (!modeManager.hasSession(player.getUniqueId())) return; modeManager.applyAdminSettings(player, flySpeed, walkSpeed, godMode); player.sendMessage(configManager.getPrefixedMessage("§aНастройки режима обновлены.")); } - public boolean performPlayerListAction(Player moderator, PlayerListAction action, String targetName) { - Player target = validateActiveTarget(moderator, targetName); - if (target == null) { + public boolean toggleVanish(Player player) { + return modeManager.toggleVanish(player); + } + + public boolean isVanished(UUID playerId) { + return modeManager.isVanished(playerId); + } + + public boolean openBanList(Player player) { + return dialogService != null && dialogService.openBanListDialog(player); + } + + public boolean toggleFreeze(Player player) { + if (freezeManager == null) { return false; } + return freezeManager.toggleFreeze(player); + } + + public boolean isFrozen(UUID playerId) { + return freezeManager != null && freezeManager.isFrozen(playerId); + } + + public boolean performPlayerListAction(Player moderator, PlayerListAction action, String targetName) { + Player target = validateActiveTarget(moderator, targetName); + if (target == null) return false; return switch (action) { case SPECTATE -> modeManager.spectatePlayer(moderator, target); case TELEPORT -> modeManager.teleportToPlayer(moderator, target); case INVSEE -> modeManager.openInvsee(moderator, target); + case MANAGE -> { + dialogService.openManagePlayerDialog(moderator, target); + yield true; + } }; } - public boolean teleportToPlayer(Player moderator, String targetName) { - Player target = validateActiveTarget(moderator, targetName); - return target != null && modeManager.teleportToPlayer(moderator, target); - } - public boolean teleportPlayerHere(Player moderator, String targetName) { - if (!validateHardActionPlayer(moderator)) { + if (!modeManager.hasSession(moderator.getUniqueId())) { + moderator.sendMessage(configManager.getPrefixedMessage("§cСначала войдите в режим модерации.")); return false; } - Player target = resolveOnlinePlayer(targetName, moderator); return target != null && modeManager.teleportPlayerHere(moderator, target); } @@ -195,77 +237,242 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return false; } - Player target = resolveOnlinePlayer(targetName, sender); - if (target == null) { + if (target == null) return false; + if (!forceDisableAbilities(target)) { + sender.sendMessage(configManager.getPrefixedMessage("§eУ игрока нет активных состояний AdminMode для сброса.")); return false; } - - forceDisableAbilities(target); sender.sendMessage(configManager.getPrefixedMessage("§aРежим и способности игрока §f" + target.getName() + "§a сброшены.")); target.sendMessage(configManager.getPrefixedMessage("§eВаш режим модерации был принудительно отключен.")); return true; } public boolean executeCPLookup(Player player, String targetPlayer, int radius, String time) { - if (!validateHardActionPlayer(player)) { - return false; - } + if (!modeManager.hasSession(player.getUniqueId())) return false; return coreProtectIntegration.lookup(player, targetPlayer, radius, time); } - public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer) { - if (!validateHardActionPlayer(player)) { - return false; - } - return coreProtectIntegration.rollback(player, radius, time, targetPlayer); + public boolean executeCPLookup(Player player, String targetPlayer, String action, String block, String exclude, + String world, int radius, String time) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.lookup(player, targetPlayer, action, block, exclude, world, radius, time); + } + + public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer, String action, String block, String exclude) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.rollback(player, radius, time, targetPlayer, action, block, exclude); + } + + public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.rollback(player, radius, time, targetPlayer, action, block, exclude, world, extraFilters); + } + + public boolean executeCPRestore(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.restore(player, radius, time, targetPlayer, action, block, exclude, world, extraFilters); } public boolean toggleInspector(Player player) { - if (!validateHardActionPlayer(player)) { - return false; - } + if (!modeManager.hasSession(player.getUniqueId())) return false; return coreProtectIntegration.toggleInspector(player); } - public void forceDisableAbilities(Player player) { + public boolean forceDisableAbilities(Player player) { + boolean changed = false; + if (modeManager.hasSession(player.getUniqueId())) { - modeManager.exitMode(player); - return; + changed = modeManager.exitMode(player); } - player.setInvulnerable(false); - player.setFlying(false); - player.setAllowFlight(false); - player.setFlySpeed(0.1f); - player.setWalkSpeed(0.2f); + if (belzeBoolManager != null) { + changed = belzeBoolManager.forceDisable(player) || changed; + } + + return changed; } public boolean canUseHardActions(CommandSender sender) { return modeManager.canUseHardActions(sender); } - private boolean openRelevantDialog(Player player) { - if (dialogService == null || !configManager.isDialogsEnabled()) { - return false; - } - - if (modeManager.hasSession(player.getUniqueId())) { - return dialogService.openCurrentPanel(player); - } - - return dialogService.openModeSelectDialog(player); + public boolean canManageModeratorAccess(Player actor, Player target) { + return actor != null + && target != null + && actor.hasPermission("adminmode.chief") + && !actor.getUniqueId().equals(target.getUniqueId()) + && target.hasPermission("adminmode.use"); } - private boolean validateHardActionPlayer(Player moderator) { - if (!canUseHardActions(moderator)) { - moderator.sendMessage(configManager.getPrefixedMessage("§cДля этого действия нужен активный профиль Hard Moder.")); + public boolean isModeratorRestricted(UUID playerId) { + return moderatorAccessManager.isRestricted(playerId); + } + + public boolean restrictModerator(Player actor, OfflinePlayer target, String durationInput, String reason) { + if (actor == null || target == null || !actor.hasPermission("adminmode.chief")) { + return false; + } + return applyRestriction(actor, target, durationInput, reason); + } + + public boolean unrestrictModerator(Player actor, OfflinePlayer target) { + if (actor == null || target == null || !actor.hasPermission("adminmode.chief")) { + return false; + } + return clearRestriction(actor, target); + } + + private boolean handleEnableMode(Player player) { + if (moderatorAccessManager.isRestricted(player.getUniqueId())) { + player.sendMessage(configManager.getPrefixedMessage(moderatorAccessManager.describeRestriction(player.getUniqueId()))); + return true; + } + if (isInAdminMode(player.getUniqueId())) { + if (!openRelevantDialog(player)) { + player.sendMessage(configManager.getPrefixedMessage("§eAdmin mode уже включён.")); + } + return true; + } + if (!modeManager.enterMode(player)) { + player.sendMessage(configManager.getPrefixedMessage("§cНе удалось включить admin mode. Проверьте права и состояние игрока.")); + } + return true; + } + + private boolean handleDisableMode(Player player) { + if (!isInAdminMode(player.getUniqueId())) { + player.sendMessage(configManager.getPrefixedMessage("§eAdmin mode уже выключен.")); + return true; + } + modeManager.exitMode(player); + return true; + } + + private boolean handleHardToggle(Player player) { + if (isInAdminMode(player.getUniqueId())) { + return handleDisableMode(player); + } + return handleEnableMode(player); + } + + private boolean applyRestriction(CommandSender sender, OfflinePlayer target, String durationInput, String reason) { + Long expiresAt = ModeratorAccessManager.parseDurationToExpiry(durationInput); + if (expiresAt == Long.MIN_VALUE) { + sender.sendMessage(configManager.getPrefixedMessage("§cНеверный срок. Примеры: 30s, 30m, 2h, 7d, 1y, perm")); return false; } + String targetName = target.getName() != null ? target.getName() : target.getUniqueId().toString(); + ModeratorAccessManager.RestrictionEntry entry = moderatorAccessManager.restrict( + target, + targetName, + sender.getName(), + expiresAt, + reason); + + Player onlineTarget = Bukkit.getPlayer(target.getUniqueId()); + if (onlineTarget != null) { + forceDisableAbilities(onlineTarget); + onlineTarget.sendMessage(configManager.getPrefixedMessage(moderatorAccessManager.describeRestriction(onlineTarget.getUniqueId()))); + } + + sender.sendMessage(configManager.getPrefixedMessage( + "§aДоступ к admin mode для §f" + entry.getPlayerName() + "§a " + + (entry.isPermanent() ? "отключён навсегда." : "ограничен по времени."))); return true; } + private boolean clearRestriction(CommandSender sender, OfflinePlayer target) { + if (!moderatorAccessManager.clearRestriction(target.getUniqueId())) { + sender.sendMessage(configManager.getPrefixedMessage("§eДля этого игрока нет активного ограничения.")); + return false; + } + + String targetName = target.getName() != null ? target.getName() : target.getUniqueId().toString(); + sender.sendMessage(configManager.getPrefixedMessage("§aОграничение для §f" + targetName + "§a снято.")); + Player onlineTarget = Bukkit.getPlayer(target.getUniqueId()); + if (onlineTarget != null) { + onlineTarget.sendMessage(configManager.getPrefixedMessage("§aДоступ к admin mode снова разрешён.")); + } + return true; + } + + private void handleRestrictCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 3) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode restrict <игрок> <1s..1y|perm> [причина]")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (target.getUniqueId() == null) { + sender.sendMessage(configManager.getPrefixedMessage("§cНе удалось определить игрока.")); + return; + } + + applyRestriction(sender, target, args[2], joinArgs(args, 3)); + } + + private void handleUnrestrictCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 2) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode unrestrict <игрок>")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + clearRestriction(sender, target); + } + + private void handleStatusCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 2) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode status <игрок>")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!moderatorAccessManager.isRestricted(target.getUniqueId())) { + sender.sendMessage(configManager.getPrefixedMessage("§aУ игрока нет ограничений на admin mode.")); + return; + } + + sender.sendMessage(configManager.getPrefixedMessage("§eСтатус для §f" + (target.getName() != null ? target.getName() : args[1]) + "§e: " + + moderatorAccessManager.describeRestriction(target.getUniqueId()))); + } + + private String joinArgs(String[] args, int startIndex) { + if (startIndex >= args.length) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (int i = startIndex; i < args.length; i++) { + if (i > startIndex) { + builder.append(' '); + } + builder.append(args[i]); + } + return builder.toString(); + } + + private boolean openRelevantDialog(Player player) { + if (dialogService == null || !configManager.isDialogsEnabled()) return false; + if (dialogPreferenceStore == null || !dialogPreferenceStore.isEnabled(player.getUniqueId())) return false; + return dialogService.openAdminPanel(player); + } + private Player validateActiveTarget(Player moderator, String targetName) { if (!modeManager.hasSession(moderator.getUniqueId())) { moderator.sendMessage(configManager.getPrefixedMessage("§cСначала войдите в режим модерации.")); @@ -279,17 +486,83 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage("§cУкажите ник игрока.")); return null; } - Player target = Bukkit.getPlayerExact(targetName.trim()); - if (target == null) { - target = Bukkit.getPlayer(targetName.trim()); - } - + if (target == null) target = Bukkit.getPlayer(targetName.trim()); if (target == null) { sender.sendMessage(configManager.getPrefixedMessage("§cИгрок не найден!")); return null; } - return target; } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (command.getName().equalsIgnoreCase("adminmode")) { + return completeAdminMode(sender, args); + } + return Collections.emptyList(); + } + + private List completeAdminMode(CommandSender sender, String[] args) { + if (args.length == 1) { + List suggestions = new ArrayList<>(); + suggestions.add("on"); + suggestions.add("off"); + suggestions.add("toggle"); + suggestions.add("dialog"); + suggestions.add("disable"); + suggestions.add("restrict"); + suggestions.add("unrestrict"); + suggestions.add("status"); + suggestions.add("reload"); + return filterPrefix(suggestions, args[0]); + } + + String subcommand = args[0].toLowerCase(); + if (args.length == 2) { + return switch (subcommand) { + case "disable" -> filterPrefix(getOnlinePlayerNames(), args[1]); + case "restrict", "status" -> filterPrefix(getKnownPlayerNames(), args[1]); + case "unrestrict" -> filterPrefix(moderatorAccessManager.getRestrictedPlayerNames(), args[1]); + default -> Collections.emptyList(); + }; + } + + if (args.length == 3 && subcommand.equals("restrict")) { + return filterPrefix(List.of("30s", "1m", "30m", "2h", "1d", "7d", "1y", "perm"), args[2]); + } + + return Collections.emptyList(); + } + + private List getOnlinePlayerNames() { + List names = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + names.add(player.getName()); + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + private List getKnownPlayerNames() { + List names = new ArrayList<>(getOnlinePlayerNames()); + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + if (offlinePlayer.getName() != null && !offlinePlayer.getName().isBlank() && !names.contains(offlinePlayer.getName())) { + names.add(offlinePlayer.getName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + private List filterPrefix(List source, String prefix) { + String normalized = prefix == null ? "" : prefix.toLowerCase(); + List result = new ArrayList<>(); + for (String value : source) { + if (normalized.isEmpty() || value.toLowerCase().startsWith(normalized)) { + result.add(value); + } + } + return result; + } } diff --git a/src/main/java/org/blz/adminmode/config/ConfigManager.java b/src/main/java/org/blz/adminmode/config/ConfigManager.java old mode 100644 new mode 100755 index c51f9b6..3b32239 --- a/src/main/java/org/blz/adminmode/config/ConfigManager.java +++ b/src/main/java/org/blz/adminmode/config/ConfigManager.java @@ -1,11 +1,10 @@ package org.blz.adminmode.config; import java.util.ArrayList; -import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.moderation.PunishmentDuration; import org.bukkit.Material; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.Plugin; @@ -17,20 +16,28 @@ public class ConfigManager { private boolean enabled; private boolean useLuckPerms; - private Map> luckPermsGroups; + private List adminLuckPermsGroups; private boolean removeGroupsOnDisable; private boolean dialogsEnabled; private String baseGroup; - private String softGroup; - private String hardGroup; - private Material softPanelMaterial; - private Material hardPanelMaterial; + private String adminGroup; + private Material panelMaterial; private Material cpInspectorMaterial; private boolean coreProtectEnabled; private int coreProtectDefaultRadius; private String coreProtectDefaultTime; private boolean sessionLoggingEnabled; private String sessionLogFile; + private boolean moderationActionsAsConsole; + private String warnCommandTemplate; + private String muteCommandTemplate; + private String unmuteCommandTemplate; + private String banCommandTemplate; + private String kickCommandTemplate; + private List punishmentDurationOptions; + private String defaultWarnDuration; + private String defaultMuteDuration; + private String defaultBanDuration; private boolean preventItemDrop; private boolean preventItemPickup; @@ -46,6 +53,7 @@ public class ConfigManager { private boolean mobsIgnorePlayer; private boolean allowFlightInSurvival; private boolean godMode; + private boolean chiefCreativeMode; private float customFlySpeed; private float customWalkSpeed; private List allowedGameModes; @@ -63,8 +71,7 @@ public class ConfigManager { private String msgBlockPlaceDenied; private String msgReloaded; private String msgPrefix; - private String msgEnterSoft; - private String msgEnterHard; + private String msgEnter; private String msgExitMode; private String msgCoreProtectUnavailable; private String msgSessionRecovered; @@ -79,17 +86,20 @@ public class ConfigManager { config = plugin.getConfig(); enabled = config.getBoolean("admin_mode.enabled", true); - useLuckPerms = config.getBoolean("admin_mode.luckperms.enabled", true); removeGroupsOnDisable = config.getBoolean("admin_mode.luckperms.remove_on_disable", true); dialogsEnabled = config.getBoolean("admin_mode.dialogs.enabled", true); baseGroup = config.getString("groups.base", "moder"); - softGroup = config.getString("groups.soft", "soft_moder_adm"); - hardGroup = config.getString("groups.hard", "hard_moder_adm"); + adminGroup = config.getString("groups.admin", "admin_moder"); - softPanelMaterial = readMaterial("items.soft_panel_material", Material.COMPASS); - hardPanelMaterial = readMaterial("items.hard_panel_material", Material.NETHER_STAR); + List groups = config.getStringList("admin_mode.luckperms.groups"); + if (groups.isEmpty()) { + groups = List.of(adminGroup); + } + adminLuckPermsGroups = new ArrayList<>(groups); + + panelMaterial = readMaterial("items.panel_material", Material.NETHER_STAR); cpInspectorMaterial = readMaterial("items.cp_inspector_material", Material.STICK); coreProtectEnabled = config.getBoolean("coreprotect.enabled", true); @@ -98,42 +108,38 @@ public class ConfigManager { sessionLoggingEnabled = config.getBoolean("logging.enabled", true); sessionLogFile = config.getString("logging.log_file", "logs/sessions.log"); + moderationActionsAsConsole = config.getBoolean("moderation_actions.dispatch_as_console", true); + punishmentDurationOptions = readPunishmentDurations(); + defaultWarnDuration = readDefaultPunishmentDuration("warn", "7d"); + defaultMuteDuration = readDefaultPunishmentDuration("mute", "1h"); + defaultBanDuration = readDefaultPunishmentDuration("ban", "7d"); + warnCommandTemplate = readModerationCommand("warn", + "warn {target} {reason}", "warn {target} {duration} {reason}", true); + muteCommandTemplate = readModerationCommand("mute", + "mute {target} {reason}", "mute {target} {duration} actor:{moderator} {reason}", true); + unmuteCommandTemplate = readModerationCommand("unmute", + "unmute {target} actor:{moderator} {reason}", + "moderationcommands:unmute {target} actor:{moderator} {reason}", false); + banCommandTemplate = readModerationCommand("ban", + "ban {target} {reason}", "ban {target} {duration} actor:{moderator} {reason}", true); + kickCommandTemplate = readModerationCommand("kick", + "kick {target} {reason}", "kick {target} actor:{moderator} {reason}", false); - List legacyGroups = config.getStringList("admin_mode.luckperms.groups"); - List softGroups = config.getStringList("admin_mode.luckperms.profile_groups.soft_moder"); - List hardGroups = config.getStringList("admin_mode.luckperms.profile_groups.hard_moder"); - if (softGroups.isEmpty()) { - if (!legacyGroups.isEmpty()) { - softGroups = new ArrayList<>(legacyGroups); - } else { - softGroups = new ArrayList<>(List.of(softGroup)); - } - } - if (hardGroups.isEmpty()) { - if (!legacyGroups.isEmpty()) { - hardGroups = new ArrayList<>(legacyGroups); - } else { - hardGroups = new ArrayList<>(List.of(hardGroup)); - } - } - luckPermsGroups = new EnumMap<>(ModeratorProfile.class); - luckPermsGroups.put(ModeratorProfile.SOFT_MODER, new ArrayList<>(softGroups)); - luckPermsGroups.put(ModeratorProfile.HARD_MODER, new ArrayList<>(hardGroups)); - - preventItemDrop = config.getBoolean("admin_mode.restrictions.prevent_item_drop", true); + preventItemDrop = config.getBoolean("admin_mode.restrictions.prevent_item_drop", false); preventItemPickup = config.getBoolean("admin_mode.restrictions.prevent_item_pickup", false); preventContainerTransfer = config.getBoolean("admin_mode.restrictions.prevent_container_transfer", true); - preventItemUse = config.getBoolean("admin_mode.restrictions.prevent_item_use", true); + preventItemUse = config.getBoolean("admin_mode.restrictions.prevent_item_use", false); allowOwnInventory = config.getBoolean("admin_mode.restrictions.allow_own_inventory", true); allowedBlocks = config.getStringList("admin_mode.restrictions.allowed_blocks"); allowedPlaceBlocks = config.getStringList("admin_mode.restrictions.allowed_place_blocks"); - givePresetBlocks = config.getBoolean("admin_mode.restrictions.give_preset_blocks", true); + givePresetBlocks = config.getBoolean("admin_mode.restrictions.give_preset_blocks", false); presetBlocks = config.getStringList("admin_mode.restrictions.preset_blocks"); forceSpectator = config.getBoolean("admin_mode.gamemode.force_spectator", false); mobsIgnorePlayer = config.getBoolean("admin_mode.gamemode.mobs_ignore_player", true); allowFlightInSurvival = config.getBoolean("admin_mode.gamemode.allow_flight_in_survival", true); godMode = config.getBoolean("admin_mode.gamemode.god_mode", true); + chiefCreativeMode = config.getBoolean("admin_mode.gamemode.chief_creative", true); customFlySpeed = (float) config.getDouble("admin_mode.gamemode.custom_fly_speed", 0.2); customWalkSpeed = (float) config.getDouble("admin_mode.gamemode.custom_walk_speed", 0.2); allowedGameModes = config.getStringList("admin_mode.gamemode.allowed_gamemodes"); @@ -143,8 +149,7 @@ public class ConfigManager { msgPermissionsGranted = config.getString("admin_mode.messages.permissions_granted", "⚠ Вам выданы временные права администратора!"); msgDisabled = config.getString("admin_mode.messages.disabled", "✓ Режим администратора отключен!"); - msgDisabledSubtitle = config.getString("admin_mode.messages.disabled_subtitle", - "Ваше состояние восстановлено."); + msgDisabledSubtitle = config.getString("admin_mode.messages.disabled_subtitle", "Ваше состояние восстановлено."); msgPermissionsRemoved = config.getString("admin_mode.messages.permissions_removed", "✓ Права администратора отозваны!"); msgNoPermission = config.getString("admin_mode.messages.no_permission", @@ -160,241 +165,158 @@ public class ConfigManager { msgReloaded = config.getString("admin_mode.messages.reloaded", "✓ Конфигурация AdminMode перезагружена!"); msgPrefix = config.getString("messages.prefix", "§8[§bAdminMode§8] "); - msgEnterSoft = config.getString("messages.enter_soft", "§aВошёл в режим §bSOFT MODER"); - msgEnterHard = config.getString("messages.enter_hard", "§aВошёл в режим §cHARD MODER"); + msgEnter = config.getString("messages.enter", "§aВошёл в режим §cADMIN MODE"); msgExitMode = config.getString("messages.exit", "§7Режим деактивирован. Состояние восстановлено."); msgCoreProtectUnavailable = config.getString("messages.cp_unavailable", - "§cCoreProtect не найден. Hard режим работает без CP."); + "§cCoreProtect не найден или отключён."); msgSessionRecovered = config.getString("messages.session_recovered", "§eВаша активная сессия модерации была восстановлена и корректно завершена."); + + validatePresetBlocks(); } - public boolean isEnabled() { - return enabled; - } + // --- Getters --- - public boolean isUseLuckPerms() { - return useLuckPerms; - } - - public List getLuckPermsGroups() { - return getLuckPermsGroups(ModeratorProfile.SOFT_MODER); - } - - public List getLuckPermsGroups(ModeratorProfile profile) { - return new ArrayList<>(luckPermsGroups.getOrDefault(profile, List.of())); - } - - public boolean isRemoveGroupsOnDisable() { - return removeGroupsOnDisable; - } - - public boolean isDialogsEnabled() { - return dialogsEnabled; - } - - public String getBaseGroup() { - return baseGroup; - } - - public String getSoftGroup() { - return softGroup; - } - - public String getHardGroup() { - return hardGroup; - } - - public String getActiveGroup(ModeratorProfile profile) { - return profile != null && profile.isHardProfile() ? hardGroup : softGroup; - } - - public Material getSoftPanelMaterial() { - return softPanelMaterial; - } - - public Material getHardPanelMaterial() { - return hardPanelMaterial; - } - - public Material getCpInspectorMaterial() { - return cpInspectorMaterial; - } - - public boolean isCoreProtectEnabled() { - return coreProtectEnabled; - } - - public int getCoreProtectDefaultRadius() { - return coreProtectDefaultRadius; - } - - public String getCoreProtectDefaultTime() { - return coreProtectDefaultTime; - } - - public boolean isSessionLoggingEnabled() { - return sessionLoggingEnabled; - } - - public String getSessionLogFile() { - return sessionLogFile; - } - - public boolean isPreventItemDrop() { - return preventItemDrop; - } - - public boolean isPreventItemPickup() { - return preventItemPickup; - } - - public boolean isPreventContainerTransfer() { - return preventContainerTransfer; - } - - public boolean isPreventItemUse() { - return preventItemUse; - } - - public boolean isAllowOwnInventory() { - return allowOwnInventory; - } - - public List getAllowedBlocks() { - return allowedBlocks; - } - - public List getAllowedPlaceBlocks() { - return allowedPlaceBlocks; - } - - public boolean isGivePresetBlocks() { - return givePresetBlocks; - } - - public List getPresetBlocks() { - return presetBlocks; - } - - public boolean isForceSpectator() { - return forceSpectator; - } - - public boolean isMobsIgnorePlayer() { - return mobsIgnorePlayer; - } - - public boolean isAllowFlightInSurvival() { - return allowFlightInSurvival; - } - - public boolean isGodMode() { - return godMode; - } - - public float getCustomFlySpeed() { - return customFlySpeed; - } - - public float getCustomWalkSpeed() { - return customWalkSpeed; - } - - public List getAllowedGameModes() { - return allowedGameModes; - } - - public String getMsgEnabled() { - return msgEnabled; - } - - public String getMsgEnabledSubtitle() { - return msgEnabledSubtitle; - } - - public String getMsgPermissionsGranted() { - return msgPermissionsGranted; - } - - public String getMsgDisabled() { - return msgDisabled; - } - - public String getMsgDisabledSubtitle() { - return msgDisabledSubtitle; - } - - public String getMsgPermissionsRemoved() { - return msgPermissionsRemoved; - } - - public String getMsgNoPermission() { - return msgNoPermission; - } - - public String getMsgItemDropDenied() { - return msgItemDropDenied; - } - - public String getMsgContainerTransferDenied() { - return msgContainerTransferDenied; - } - - public String getMsgItemUseDenied() { - return msgItemUseDenied; - } - - public String getMsgBlockPlaceDenied() { - return msgBlockPlaceDenied; - } - - public String getMsgReloaded() { - return msgReloaded; - } - - public String getMsgPrefix() { - return msgPrefix; - } - - public String getMsgEnterSoft() { - return msgEnterSoft; - } - - public String getMsgEnterHard() { - return msgEnterHard; - } - - public String getMsgExitMode() { - return msgExitMode; - } - - public String getMsgCoreProtectUnavailable() { - return msgCoreProtectUnavailable; - } - - public String getMsgSessionRecovered() { - return msgSessionRecovered; + public boolean isEnabled() { return enabled; } + public boolean isUseLuckPerms() { return useLuckPerms; } + public List getLuckPermsGroups() { return new ArrayList<>(adminLuckPermsGroups); } + public boolean isRemoveGroupsOnDisable() { return removeGroupsOnDisable; } + public boolean isDialogsEnabled() { return dialogsEnabled; } + public String getBaseGroup() { return baseGroup; } + public String getAdminGroup() { return adminGroup; } + public Material getPanelMaterial() { return panelMaterial; } + public Material getCpInspectorMaterial() { return cpInspectorMaterial; } + public boolean isCoreProtectEnabled() { return coreProtectEnabled; } + public int getCoreProtectDefaultRadius() { return coreProtectDefaultRadius; } + public String getCoreProtectDefaultTime() { return coreProtectDefaultTime; } + public boolean isSessionLoggingEnabled() { return sessionLoggingEnabled; } + public String getSessionLogFile() { return sessionLogFile; } + public boolean isModerationActionsAsConsole() { return moderationActionsAsConsole; } + public String getWarnCommandTemplate() { return warnCommandTemplate; } + public String getMuteCommandTemplate() { return muteCommandTemplate; } + public String getUnmuteCommandTemplate() { return unmuteCommandTemplate; } + public String getBanCommandTemplate() { return banCommandTemplate; } + public String getKickCommandTemplate() { return kickCommandTemplate; } + public List getPunishmentDurationOptions() { return new ArrayList<>(punishmentDurationOptions); } + public String getDefaultWarnDuration() { return defaultWarnDuration; } + public String getDefaultMuteDuration() { return defaultMuteDuration; } + public String getDefaultBanDuration() { return defaultBanDuration; } + public boolean isPunishmentDurationAllowed(String value) { + String normalized = PunishmentDuration.normalizeToken(value); + return !normalized.isEmpty() && punishmentDurationOptions.contains(normalized); } + public boolean isPreventItemDrop() { return preventItemDrop; } + public boolean isPreventItemPickup() { return preventItemPickup; } + public boolean isPreventContainerTransfer() { return preventContainerTransfer; } + public boolean isPreventItemUse() { return preventItemUse; } + public boolean isAllowOwnInventory() { return allowOwnInventory; } + public List getAllowedBlocks() { return allowedBlocks; } + public List getAllowedPlaceBlocks() { return allowedPlaceBlocks; } + public boolean isGivePresetBlocks() { return givePresetBlocks; } + public List getPresetBlocks() { return presetBlocks; } + public boolean isForceSpectator() { return forceSpectator; } + public boolean isMobsIgnorePlayer() { return mobsIgnorePlayer; } + public boolean isAllowFlightInSurvival() { return allowFlightInSurvival; } + public boolean isGodMode() { return godMode; } + public boolean isChiefCreativeMode() { return chiefCreativeMode; } + public float getCustomFlySpeed() { return customFlySpeed; } + public float getCustomWalkSpeed() { return customWalkSpeed; } + public List getAllowedGameModes() { return allowedGameModes; } + public String getMsgEnabled() { return msgEnabled; } + public String getMsgEnabledSubtitle() { return msgEnabledSubtitle; } + public String getMsgPermissionsGranted() { return msgPermissionsGranted; } + public String getMsgDisabled() { return msgDisabled; } + public String getMsgDisabledSubtitle() { return msgDisabledSubtitle; } + public String getMsgPermissionsRemoved() { return msgPermissionsRemoved; } + public String getMsgNoPermission() { return msgNoPermission; } + public String getMsgItemDropDenied() { return msgItemDropDenied; } + public String getMsgContainerTransferDenied() { return msgContainerTransferDenied; } + public String getMsgItemUseDenied() { return msgItemUseDenied; } + public String getMsgBlockPlaceDenied() { return msgBlockPlaceDenied; } + public String getMsgReloaded() { return msgReloaded; } + public String getMsgPrefix() { return msgPrefix; } + public String getMsgEnter() { return msgEnter; } + public String getMsgExitMode() { return msgExitMode; } + public String getMsgCoreProtectUnavailable() { return msgCoreProtectUnavailable; } + public String getMsgSessionRecovered() { return msgSessionRecovered; } public String getPrefixedMessage(String message) { - if (message == null || message.isEmpty()) { - return msgPrefix; - } + if (message == null || message.isEmpty()) return msgPrefix; return msgPrefix + message; } private Material readMaterial(String path, Material fallback) { String value = config.getString(path); - if (value == null || value.isBlank()) { - return fallback; - } - + if (value == null || value.isBlank()) return fallback; try { return Material.valueOf(value.trim().toUpperCase()); - } catch (IllegalArgumentException exception) { + } catch (IllegalArgumentException e) { plugin.getLogger().warning("Неизвестный material в конфиге " + path + ": " + value); return fallback; } } + + private void validatePresetBlocks() { + for (String blockString : presetBlocks) { + try { + String[] parts = blockString.split(":"); + Material.valueOf(parts[0].trim().toUpperCase()); + } catch (Exception e) { + plugin.getLogger().warning("Неверный блок в preset_blocks: " + blockString); + } + } + } + + private List readPunishmentDurations() { + List configured = config.getStringList("moderation_actions.durations.options"); + if (configured.isEmpty()) { + configured = List.of("1h", "1d", "7d", "30d", "permanent"); + } + + LinkedHashSet normalized = new LinkedHashSet<>(); + for (String value : configured) { + String token = PunishmentDuration.normalizeToken(value); + if (token.isEmpty()) { + plugin.getLogger().warning("Неизвестный срок наказания в config.yml: " + value); + } else { + normalized.add(token); + } + } + if (normalized.isEmpty()) { + normalized.addAll(List.of("1h", "1d", "7d", "30d", "permanent")); + } + return new ArrayList<>(normalized); + } + + private String readDefaultPunishmentDuration(String action, String fallback) { + String configured = config.getString("moderation_actions.durations.defaults." + action, fallback); + String normalized = PunishmentDuration.normalizeToken(configured); + if (!normalized.isEmpty() && punishmentDurationOptions.contains(normalized)) { + return normalized; + } + plugin.getLogger().warning("Срок по умолчанию для " + action + " отсутствует в списке options: " + configured); + return punishmentDurationOptions.contains(fallback) ? fallback : punishmentDurationOptions.getFirst(); + } + + private String readModerationCommand(String action, String legacyTemplate, String newTemplate, boolean requiresDuration) { + String path = "moderation_actions.commands." + action; + String configured = config.getString(path, newTemplate); + if (configured == null || configured.isBlank()) { + return ""; + } + configured = configured.trim(); + if (configured.equals(legacyTemplate)) { + plugin.getLogger().info("Шаблон команды " + action + " обновлён в памяти до формата наказаний со сроком/автором."); + configured = newTemplate; + } + if (requiresDuration && !configured.contains("{duration}")) { + plugin.getLogger().warning("Команда " + action + " отключена: шаблон " + path + " должен содержать {duration}."); + return ""; + } + if (!configured.contains("{target}")) { + plugin.getLogger().warning("Команда " + action + " отключена: шаблон " + path + " должен содержать {target}."); + return ""; + } + return configured; + } } diff --git a/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java b/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java old mode 100644 new mode 100755 index bf57a3c..0de361e --- a/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java +++ b/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java @@ -1,29 +1,23 @@ package org.blz.adminmode.data; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.UUID; +import org.blz.adminmode.utils.ItemStackCodec; +import org.blz.adminmode.utils.PotionEffectCodec; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -187,16 +181,7 @@ public class AdminModeDataManager { private String serializeItemArray(ItemStack[] items) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); - dataOutput.writeInt(items.length); - - for (ItemStack item : items) { - dataOutput.writeObject(item); - } - - dataOutput.close(); - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItems(items); } catch (Exception e) { e.printStackTrace(); return ""; @@ -205,17 +190,7 @@ public class AdminModeDataManager { private ItemStack[] deserializeItemArray(String data) { try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - int length = dataInput.readInt(); - ItemStack[] items = new ItemStack[length]; - - for (int i = 0; i < length; i++) { - items[i] = (ItemStack) dataInput.readObject(); - } - - dataInput.close(); - return items; + return ItemStackCodec.deserializeItems(data); } catch (Exception e) { e.printStackTrace(); return new ItemStack[0]; @@ -224,11 +199,7 @@ public class AdminModeDataManager { private String serializeItem(ItemStack item) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); - dataOutput.writeObject(item); - dataOutput.close(); - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItem(item); } catch (Exception e) { e.printStackTrace(); return ""; @@ -238,56 +209,23 @@ public class AdminModeDataManager { private ItemStack deserializeItem(String data) { try { if (data.isEmpty()) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - ItemStack item = (ItemStack) dataInput.readObject(); - dataInput.close(); - return item; + return ItemStackCodec.deserializeItem(data); } catch (Exception e) { e.printStackTrace(); - return new ItemStack(Material.AIR); + return ItemStack.empty(); } } private String serializePotionEffects(Collection effects) { - StringBuilder sb = new StringBuilder(); - for (PotionEffect effect : effects) { - sb.append(effect.getType().getName()).append(":") - .append(effect.getDuration()).append(":") - .append(effect.getAmplifier()).append(":") - .append(effect.isAmbient()).append(":") - .append(effect.hasParticles()).append(":") - .append(effect.hasIcon()).append(";"); - } - return sb.toString(); + return PotionEffectCodec.serialize(effects); } private Collection deserializePotionEffects(String data) { - Collection effects = new ArrayList<>(); - if (data.isEmpty()) return effects; - - String[] effectStrings = data.split(";"); - for (String effectStr : effectStrings) { - if (effectStr.isEmpty()) continue; - - try { - String[] parts = effectStr.split(":"); - PotionEffectType type = PotionEffectType.getByName(parts[0]); - int duration = Integer.parseInt(parts[1]); - int amplifier = Integer.parseInt(parts[2]); - boolean ambient = Boolean.parseBoolean(parts[3]); - boolean particles = Boolean.parseBoolean(parts[4]); - boolean icon = Boolean.parseBoolean(parts[5]); - - effects.add(new PotionEffect(type, duration, amplifier, ambient, particles, icon)); - } catch (Exception e) { - plugin.getLogger().warning("Ошибка десериализации эффекта: " + effectStr); - } - } - - return effects; + return PotionEffectCodec.deserialize(data, + encodedEffect -> plugin.getLogger().warning( + "Ошибка десериализации эффекта: " + encodedEffect)); } public static class PlayerStateData { diff --git a/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java b/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java old mode 100644 new mode 100755 index fae1e6c..f65ff84 --- a/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java +++ b/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java @@ -1,5 +1,7 @@ package org.blz.adminmode.dialog; +import com.destroystokyo.paper.profile.PlayerProfile; +import io.papermc.paper.ban.BanListType; import io.papermc.paper.dialog.Dialog; import io.papermc.paper.registry.data.dialog.ActionButton; import io.papermc.paper.registry.data.dialog.DialogBase; @@ -7,22 +9,32 @@ import io.papermc.paper.registry.data.dialog.action.DialogAction; import io.papermc.paper.registry.data.dialog.action.DialogActionCallback; import io.papermc.paper.registry.data.dialog.body.DialogBody; import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.input.SingleOptionDialogInput; import io.papermc.paper.registry.data.dialog.type.DialogType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.format.NamedTextColor; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; -import org.blz.adminmode.moderation.ModeratorProfile; -import org.blz.adminmode.moderation.PlayerListAction; +import org.blz.adminmode.moderation.ModeratorPreferencesStore; +import org.blz.adminmode.moderation.ModeratorWarningStore; +import org.blz.adminmode.moderation.PunishmentDuration; +import org.bukkit.BanEntry; import org.bukkit.Bukkit; -import org.bukkit.entity.Player; +import org.bukkit.GameMode; +import org.bukkit.ban.ProfileBanList; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; import java.util.List; -import java.util.UUID; +import java.util.Set; @SuppressWarnings("UnstableApiUsage") public class AdminDialogService { @@ -31,276 +43,525 @@ public class AdminDialogService { .uses(1) .lifetime(Duration.ofMinutes(5)) .build(); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .withZone(ZoneId.systemDefault()); private final Plugin plugin; private final ConfigManager configManager; private final AdminModeCommand adminModeCommand; + private final DialogPreferenceStore dialogPreferenceStore; + private final MenuEmojiPreferenceStore emojiPreferenceStore; + private final ModeratorPreferencesStore preferencesStore; + private final ModeratorWarningStore warningStore; - public AdminDialogService(Plugin plugin, ConfigManager configManager, AdminModeCommand adminModeCommand) { + public AdminDialogService( + Plugin plugin, + ConfigManager configManager, + AdminModeCommand adminModeCommand, + DialogPreferenceStore dialogPreferenceStore, + MenuEmojiPreferenceStore emojiPreferenceStore, + ModeratorPreferencesStore preferencesStore, + ModeratorWarningStore warningStore) { this.plugin = plugin; this.configManager = configManager; this.adminModeCommand = adminModeCommand; + this.dialogPreferenceStore = dialogPreferenceStore; + this.emojiPreferenceStore = emojiPreferenceStore; + this.preferencesStore = preferencesStore; + this.warningStore = warningStore; } - public boolean openModeSelectDialog(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - List profiles = adminModeCommand.getAvailableProfiles(player); - if (profiles.isEmpty()) { - player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); - return false; - } - - if (profiles.size() == 1) { - adminModeCommand.activateProfile(player, profiles.get(0)); - return true; - } + public boolean openAdminPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { List buttons = new ArrayList<>(); - for (ModeratorProfile profile : profiles) { - buttons.add(actionButton( - profile.isHardProfile() ? "🔨 Hard Moder" : "🔍 Soft Moder", - profile.isHardProfile() - ? "Полный режим модерации, CoreProtect и сильные действия" - : "Наблюдение, spectate, invsee и безопасные действия", - (response, audience) -> withPlayer(audience, moderator -> { - adminModeCommand.activateProfile(moderator, profile); - plugin.getServer().getScheduler().runTask(plugin, () -> { - if (adminModeCommand.isInAdminMode(moderator.getUniqueId())) { - openCurrentPanel(moderator); - } - }); - }))); - } + buttons.add(actionButton(player, "🎯", "Действия с игроком", NamedTextColor.RED, + "Поиск игрока и модераторские действия", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPlayerSearchDialog(m))))); + buttons.add(actionButton(player, "📋", "Список онлайн", NamedTextColor.GOLD, + "Игроки онлайн и их координаты", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openOnlineListDialog(m))))); + buttons.add(actionButton(player, "📕", "BanList", NamedTextColor.DARK_RED, + "Посмотреть список банов и причины", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanSearchDialog(m))))); + buttons.add(actionButton(player, "🧰", "CoreProtect", NamedTextColor.AQUA, + "Инспектор, lookup, rollback и restore", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectMenu(m))))); + buttons.add(actionButton(player, "⚙", "Настройки режима", NamedTextColor.GREEN, + "Полет, vanish, текущий режим и скорости", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openSettingsPanel(m))))); + buttons.add(actionButton(player, "🪪", "Личные настройки", NamedTextColor.LIGHT_PURPLE, + "Тема, акцент, смайлики и режим по умолчанию", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPersonalSettingsPanel(m))))); + buttons.add(actionButton(player, "🚪", "Выйти из режима", NamedTextColor.RED, + "Восстановить состояние и снять права", true, + (res, aud) -> withPlayer(aud, adminModeCommand::exitMode))); - buttons.add(actionButton( - "Закрыть", - "Закрыть выбор режима", - (response, audience) -> { + DialogBase base = DialogBase.builder(title(player, "🛡", "Admin Mode", NamedTextColor.AQUA)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Панель модератора: игроки, CoreProtect, BanList, режим и личные настройки."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, closeButton(player), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть admin panel dialog: " + e.getMessage()); + return false; + } + } + + public boolean openPlayerSearchDialog(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ActionButton openButton = actionButton(player, "🔎", "Открыть список", NamedTextColor.RED, + "Показать игроков по фильтру", false, + (response, audience) -> withPlayer(audience, moderator -> { + String query = normalize(response.getText("player_query")); + schedule(() -> openManagePlayerListDialog(moderator, query)); })); - DialogBase base = DialogBase.builder(Component.text("⚙ Режим модерации")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Выберите режим входа. Soft — наблюдение и безопасная модерация. Hard — полный контроль, телепорты и CoreProtect."), - 360))) - .inputs(List.of()) + DialogBase base = DialogBase.builder(title(player, "🎯", "Поиск игрока", NamedTextColor.RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Введите часть ника или оставьте пусто для полного списка."), 360))) + .inputs(List.of(DialogInput.text("player_query", 320, bodyText(player, "Игрок"), false, "", 32, null))) .build(); - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 1)); + showDialog(player, base, DialogType.multiAction(List.of(openButton), backButton(player, () -> openAdminPanel(player)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть dialog выбора профиля: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть поиск игрока: " + e.getMessage()); return false; } } - public boolean openCurrentPanel(Player player) { - UUID playerId = player.getUniqueId(); - ModeratorProfile profile = adminModeCommand.getActiveProfile(playerId); - if (profile == null) { - return false; - } - - if (profile.isHardProfile()) { - return openHardPanel(player); - } - - return openSoftPanel(player); - } - - public boolean openSoftPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } + public boolean openManagePlayerListDialog(Player moderator, String query) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; try { - List buttons = new ArrayList<>(); - buttons.add(actionButton( - "👁 Spectate игрока", - "Выбрать игрока для режима наблюдения", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.SPECTATE))))); - buttons.add(actionButton( - "📍 TP к игроку", - "Выбрать игрока для телепортации", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.TELEPORT))))); - buttons.add(actionButton( - "🎒 Invsee игрока", - "Открыть инвентарь игрока только для просмотра", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.INVSEE))))); - buttons.add(actionButton( - "📋 Список онлайн", - "Показать онлайн-игроков и их координаты", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openOnlineListDialog(moderator))))); - buttons.add(actionButton( - "🎛 Настройки режима", - "Скорость полёта, ходьбы и бессмертие", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openSpeedPanel(moderator))))); - buttons.add(actionButton( - "🚪 Выйти из режима", - "Восстановить сохраненное состояние и снять временные права", - (response, audience) -> withPlayer(audience, moderator -> adminModeCommand.exitMode(moderator)))); - - List lines = new ArrayList<>(); - lines.add(Component.text("Soft Moder предназначен для наблюдения, инвентарей и безопасной телепортации.")); - lines.add(Component.text("Используйте компас в руке как panel item или /adminmode для повторного открытия.")); - - DialogBase base = DialogBase.builder(Component.text("🔍 Soft Moder")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 2)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть soft panel dialog: " + exception.getMessage()); - return false; - } - } - - public boolean openHardPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - - try { - List buttons = new ArrayList<>(); - buttons.add(actionButton( - "👁 Spectate игрока", - "Выбрать игрока для режима наблюдения", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.SPECTATE))))); - buttons.add(actionButton( - "📍 TP к игроку", - "Телепорт к выбранному игроку", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.TELEPORT))))); - buttons.add(actionButton( - "🎒 Invsee игрока", - "Открыть инвентарь игрока только для просмотра", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.INVSEE))))); - buttons.add(actionButton( - "📋 Список онлайн", - "Показать онлайн-игроков и координаты", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openOnlineListDialog(moderator))))); - buttons.add(actionButton( - "🔍 Lookup логи CP", - "Открыть форму поиска по логам CoreProtect", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openLookupDialog(moderator))))); - buttons.add(actionButton( - "↩ Откат области CP", - "Настроить rollback с подтверждением", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openRollbackDialog(moderator))))); - buttons.add(actionButton( - "🎛 Настройки режима", - "Скорость полёта, ходьбы и бессмертие", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openSpeedPanel(moderator))))); - buttons.add(actionButton( - "🚪 Выйти из режима", - "Восстановить сохраненное состояние и выйти из hard режима", - (response, audience) -> withPlayer(audience, moderator -> adminModeCommand.exitMode(moderator)))); - - List lines = new ArrayList<>(); - lines.add(Component.text("Hard Moder включает полный доступ к сильным действиям и CoreProtect.")); - lines.add(Component.text("Используйте Nether Star для панели и Stick для переключения co inspect.")); - - DialogBase base = DialogBase.builder(Component.text("🔨 Hard Moder")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 2)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть hard panel dialog: " + exception.getMessage()); - return false; - } - } - - public boolean openPlayerListDialog(Player player, PlayerListAction action) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - - try { - List buttons = new ArrayList<>(); + List filtered = new ArrayList<>(); for (Player target : Bukkit.getOnlinePlayers()) { - if (target.getUniqueId().equals(player.getUniqueId())) { - continue; + if (target.getUniqueId().equals(moderator.getUniqueId())) continue; + if (matchesQuery(target.getName(), query)) { + filtered.add(target); } + } + filtered.sort(Comparator.comparing(Player::getName, String.CASE_INSENSITIVE_ORDER)); + List buttons = new ArrayList<>(); + for (Player target : filtered) { buttons.add(actionButton( - target.getName(), - target.getWorld().getName() + " | " + target.getLocation().getBlockX() + ", " - + target.getLocation().getBlockY() + ", " + target.getLocation().getBlockZ(), - (response, audience) -> withPlayer(audience, moderator -> - adminModeCommand.performPlayerListAction(moderator, action, target.getName())))); + playerLabel(moderator, target.getName(), NamedTextColor.WHITE, false), + bodyText(moderator, target.getWorld().getName() + " | " + target.getLocation().getBlockX() + ", " + + target.getLocation().getBlockY() + ", " + target.getLocation().getBlockZ()), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openManagePlayerDialog(m, target))))); } if (buttons.isEmpty()) { - buttons.add(actionButton( - "Нет целей", - "Кроме вас сейчас никого нет онлайн", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator))))); + buttons.add(actionButton(playerLabel(moderator, "Никого не найдено", NamedTextColor.DARK_GRAY, false), + bodyText(moderator, "Измените фильтр поиска"), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPlayerSearchDialog(m))))); } - ActionButton backButton = actionButton( - "Назад", - "Вернуться к основной панели", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); - - DialogBase base = DialogBase.builder(Component.text(dialogTitleForAction(action))) + DialogBase base = DialogBase.builder(title(moderator, "👥", "Игроки", NamedTextColor.RED)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text(dialogBodyForAction(action)), 360))) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, query.isBlank() + ? "Все игроки онлайн." + : "Результаты поиска по фильтру: " + query), 360))) .inputs(List.of()) .build(); - showDialog(player, base, DialogType.multiAction(buttons, backButton, 2)); + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openPlayerSearchDialog(moderator)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть список игроков: " + e.getMessage()); + return false; + } + } + + public boolean openManagePlayerDialog(Player moderator, Player target) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + List recentWarnings = warningStore.getRecentWarnings(target.getUniqueId(), 3); + int activeWarnCount = warningStore.getActiveWarnings(target.getUniqueId()).size(); + + List buttons = new ArrayList<>(); + buttons.add(actionButton(moderator, "👁", "Spectate игрока", NamedTextColor.AQUA, + "Наблюдать за игроком", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.SPECTATE, target.getName())))); + buttons.add(actionButton(moderator, "📍", "TP к игроку", NamedTextColor.GREEN, + "Телепортироваться к игроку", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.TELEPORT, target.getName())))); + buttons.add(actionButton(moderator, "🎒", "InvSee игрока", NamedTextColor.GOLD, + "Открыть инвентарь игрока только для чтения", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.INVSEE, target.getName())))); + buttons.add(actionButton( + moderator, + adminModeCommand.isFrozen(target.getUniqueId()) ? "🧊" : "❄", + adminModeCommand.isFrozen(target.getUniqueId()) ? "Разморозить" : "Заморозить", + NamedTextColor.AQUA, + "Полный freeze игрока", + true, + (res, aud) -> withPlayer(aud, m -> { + boolean frozen = adminModeCommand.toggleFreeze(target); + m.sendMessage(Component.text(frozen ? "§bИгрок " + target.getName() + " заморожен." : "§aИгрок " + target.getName() + " разморожен.")); + schedule(() -> openManagePlayerDialog(m, target)); + }))); + buttons.add(actionButton(moderator, "📝", "Последние warn", NamedTextColor.YELLOW, + "Открыть последние предупреждения игрока", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnListDialog(m, target, false))))); + buttons.add(actionButton(moderator, "📌", "Активные warn", NamedTextColor.GOLD, + "Открыть активные предупреждения игрока", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnListDialog(m, target, true))))); + if (adminModeCommand.canManageModeratorAccess(moderator, target)) { + if (adminModeCommand.isModeratorRestricted(target.getUniqueId())) { + buttons.add(actionButton(moderator, "🔓", "Вернуть admin mode", NamedTextColor.GREEN, + "Снять ограничение с модератора", true, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.unrestrictModerator(m, target); + schedule(() -> openManagePlayerDialog(m, target)); + }))); + } else { + buttons.add(actionButton(moderator, "🔒", "Забрать admin mode", NamedTextColor.RED, + "Ограничить модератору доступ к admin mode", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openRestrictModeratorDialog(m, target))))); + } + } + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.WARN); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.MUTE); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.UNMUTE); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.KICK); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.BAN); + + List bodyLines = new ArrayList<>(); + bodyLines.add(bodyLine(moderator, "Мир: " + target.getWorld().getName(), false)); + bodyLines.add(bodyLine(moderator, "Режим: " + target.getGameMode().name(), false)); + if (target.hasPermission("adminmode.use")) { + bodyLines.add(bodyLine(moderator, "Модератор: да", true)); + bodyLines.add(bodyLine(moderator, + "Доступ к admin mode: " + (adminModeCommand.isModeratorRestricted(target.getUniqueId()) ? "ограничен" : "разрешён"), + true)); + } + bodyLines.add(bodyLine(moderator, "Активных warn: " + activeWarnCount, true)); + if (recentWarnings.isEmpty()) { + bodyLines.add(bodyLine(moderator, "Последние warn: нет записей", false)); + } else { + bodyLines.add(bodyLine(moderator, "Последние warn:", true)); + for (ModeratorWarningStore.WarningEntry warning : recentWarnings) { + bodyLines.add(bodyLine(moderator, "• " + formatWarnInline(warning), false)); + } + } + + DialogBase base = DialogBase.builder(title(moderator, "🎯", "Действия: " + target.getName(), NamedTextColor.RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(bodyLines), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openPlayerSearchDialog(moderator)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть manage dialog: " + e.getMessage()); + return false; + } + } + + public boolean openRestrictModeratorDialog(Player moderator, Player target) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + if (!adminModeCommand.canManageModeratorAccess(moderator, target)) return false; + try { + ActionButton applyButton = actionButton(moderator, "🔒", "Ограничить", NamedTextColor.RED, + "Снять модератору доступ к admin mode", true, + (response, audience) -> withPlayer(audience, m -> { + String duration = normalize(response.getText("restrict_duration")); + String reason = sanitizeReason(response.getText("restrict_reason")); + adminModeCommand.restrictModerator(m, target, duration, reason); + schedule(() -> openManagePlayerDialog(m, target)); + })); + + DialogBase base = DialogBase.builder(title(moderator, "🔒", "Ограничение: " + target.getName(), NamedTextColor.RED)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(List.of( + bodyLine(moderator, "Главный модератор может временно или навсегда забрать доступ к admin mode.", true), + bodyLine(moderator, "Поддерживаются сроки от 1s до 1y, либо perm.", false))), 360))) + .inputs(List.of( + DialogInput.text("restrict_duration", 180, bodyText(moderator, "Срок"), true, "30m", 16, null), + DialogInput.text("restrict_reason", 320, bodyText(moderator, "Причина"), false, "", 128, null))) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(applyButton), backButton(moderator, () -> openManagePlayerDialog(moderator, target)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог ограничения для " + target.getName() + ": " + e.getMessage()); + return false; + } + } + + public boolean openWarnListDialog(Player moderator, Player target, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + List warnings = activeOnly + ? warningStore.getActiveWarnings(target.getUniqueId()) + : warningStore.getRecentWarnings(target.getUniqueId(), 12); + + List buttons = new ArrayList<>(); + for (ModeratorWarningStore.WarningEntry warning : warnings) { + buttons.add(actionButton( + playerLabel(moderator, formatWarnHeader(warning), NamedTextColor.WHITE, true), + bodyText(moderator, warningStatusLabel(warning) + " | " + warning.reason()), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnDetailsDialog(m, target, warning, activeOnly))))); + } + + if (buttons.isEmpty()) { + buttons.add(actionButton(playerLabel(moderator, "Warn не найдены", NamedTextColor.DARK_GRAY, false), + bodyText(moderator, "У игрока пока нет записей."), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openManagePlayerDialog(m, target))))); + } + + DialogBase base = DialogBase.builder(title(moderator, activeOnly ? "📌" : "📝", + (activeOnly ? "Активные warn: " : "Последние warn: ") + target.getName(), + NamedTextColor.YELLOW)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + activeOnly ? "Список активных предупреждений игрока." : "История последних предупреждений игрока."), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openManagePlayerDialog(moderator, target)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть список warn для " + target.getName() + ": " + e.getMessage()); + return false; + } + } + + public boolean openWarnDetailsDialog(Player moderator, Player target, ModeratorWarningStore.WarningEntry warning, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + ModeratorWarningStore.WarningEntry current = warningStore.findWarning(warning.id()).orElse(null); + if (current == null) { + moderator.sendMessage(configManager.getPrefixedMessage("§eЗапись warn больше не существует.")); + schedule(() -> openWarnListDialog(moderator, target, activeOnly)); + return true; + } + + List lines = new ArrayList<>(); + lines.add(bodyLine(moderator, "Игрок: " + current.playerName(), false)); + lines.add(bodyLine(moderator, "Модератор: " + current.moderatorName(), false)); + lines.add(bodyLine(moderator, "Выдан: " + formatTimestamp(current.createdAt()), false)); + lines.add(bodyLine(moderator, "Истекает: " + formatWarnExpiration(current), false)); + lines.add(bodyLine(moderator, "Статус: " + warningStatusLabel(current), true)); + lines.add(bodyLine(moderator, "Причина: " + current.reason(), false)); + if (current.status() == ModeratorWarningStore.WarningStatus.REVOKED) { + lines.add(bodyLine(moderator, "Снят: " + formatOptionalTimestamp(current.revokedAt()), false)); + lines.add(bodyLine(moderator, "Кем снят: " + blankFallback(current.revokedBy(), "неизвестно"), false)); + lines.add(bodyLine(moderator, "Причина снятия: " + blankFallback(current.revocationReason(), "не указана"), false)); + } + + List actions = new ArrayList<>(); + if (current.status() == ModeratorWarningStore.WarningStatus.ACTIVE + && moderator.hasPermission(PunishmentAction.UNWARN.permission)) { + actions.add(actionButton(moderator, "✅", "Снять warn", NamedTextColor.GREEN, + "Архивировать активное предупреждение", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnRevocationDialog(m, target, current, activeOnly))))); + } + + DialogBase base = DialogBase.builder(title(moderator, "📄", "Warn: " + target.getName(), NamedTextColor.YELLOW)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(actions, + backButton(moderator, () -> openWarnListDialog(moderator, target, activeOnly)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть детали warn: " + e.getMessage()); + return false; + } + } + + private boolean openWarnRevocationDialog(Player moderator, Player target, + ModeratorWarningStore.WarningEntry warning, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(PunishmentAction.UNWARN.permission)) { + return false; + } + try { + ActionButton confirmButton = actionButton(moderator, "✅", "Подтвердить снятие", NamedTextColor.GREEN, + "Снять предупреждение и сохранить его в истории", true, + (response, audience) -> withPlayer(audience, m -> schedule(() -> { + if (!adminModeCommand.isInAdminMode(m.getUniqueId()) + || !m.hasPermission(PunishmentAction.UNWARN.permission)) { + m.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + String reason = sanitizeReason(response.getText("revocation_reason")); + if (reason.isBlank()) { + m.sendMessage(configManager.getPrefixedMessage("§cУкажите причину снятия warn.")); + openWarnRevocationDialog(m, target, warning, activeOnly); + return; + } + if (!warningStore.revokeWarning(warning.id(), m, reason)) { + m.sendMessage(configManager.getPrefixedMessage("§eWarn уже снят или истёк.")); + } else { + m.sendMessage(configManager.getPrefixedMessage("§aWarn для §f" + warning.playerName() + "§a снят и сохранён в истории.")); + } + openWarnListDialog(m, target, activeOnly); + }))); + + DialogBase base = DialogBase.builder(title(moderator, "✅", "Снять warn: " + warning.playerName(), NamedTextColor.GREEN)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + "Warn останется в истории со сведениями о снятии."), 360))) + .inputs(List.of(DialogInput.text("revocation_reason", 320, + bodyText(moderator, "Причина снятия"), true, "", 256, null))) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(confirmButton), + backButton(moderator, () -> openWarnDetailsDialog(moderator, target, warning, activeOnly)), 1)); return true; } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть список игроков: " + exception.getMessage()); + plugin.getLogger().warning("Не удалось открыть снятие warn: " + exception.getMessage()); return false; } } public boolean openOnlineListDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List lines = adminModeCommand.getOnlineSummary(player); + player.sendMessage(Component.text("--- Список игроков онлайн ---", themedColor(player, NamedTextColor.GOLD, true))); + for (Component line : lines) player.sendMessage(line); + player.sendMessage(Component.text("----------------------------------", themedColor(player, NamedTextColor.GOLD, true))); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось вывести онлайн список: " + e.getMessage()); return false; } + } + public boolean openBanSearchDialog(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { - List lines = new ArrayList<>(adminModeCommand.getOnlineSummary(player)); - ActionButton backButton = actionButton( - "Назад", - "Вернуться к панели режима", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); + ActionButton searchButton = actionButton(player, "🔎", "Открыть BanList", NamedTextColor.DARK_RED, + "Показать баны по фильтру", true, + (response, audience) -> withPlayer(audience, moderator -> { + String query = normalize(response.getText("ban_query")); + schedule(() -> openBanListDialog(moderator, query)); + })); - DialogBase base = DialogBase.builder(Component.text("📋 Онлайн список")) + DialogBase base = DialogBase.builder(title(player, "📕", "Поиск в BanList", NamedTextColor.DARK_RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, "Введите ник или его часть. Пусто = все баны."), 360))) + .inputs(List.of(DialogInput.text("ban_query", 320, bodyText(player, "Игрок"), false, "", 32, null))) + .build(); + + showDialog(player, base, DialogType.multiAction(List.of(searchButton), backButton(player, () -> openAdminPanel(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть поиск по банам: " + e.getMessage()); + return false; + } + } + + public boolean openBanListDialog(Player player) { + return openBanListDialog(player, ""); + } + + public boolean openBanListDialog(Player player, String query) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ProfileBanList banList = Bukkit.getBanList(BanListType.PROFILE); + Set> entries = banList.getEntries(); + List> sortedEntries = new ArrayList<>(); + for (BanEntry entry : entries) { + String targetName = profileName(entry.getBanTarget()); + if (!isExpiredBan(entry) && matchesQuery(targetName, query)) { + sortedEntries.add(entry); + } + } + sortedEntries.sort(Comparator.comparing( + entry -> profileName(entry.getBanTarget()), + String.CASE_INSENSITIVE_ORDER)); + + List buttons = new ArrayList<>(); + for (BanEntry entry : sortedEntries) { + PlayerProfile target = entry.getBanTarget(); + String targetName = profileName(target); + String targetIdentifier = profileIdentifier(target); + buttons.add(actionButton( + playerLabel(player, targetName, NamedTextColor.WHITE, true), + bodyText(player, buildBanSummary(entry)), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanDetailsDialog(m, targetIdentifier))))); + } + + if (buttons.isEmpty()) { + buttons.add(actionButton(playerLabel(player, "Баны не найдены", NamedTextColor.DARK_GRAY, false), + bodyText(player, "Измените фильтр поиска"), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanSearchDialog(m))))); + } + + DialogBase base = DialogBase.builder(title(player, "📕", "BanList", NamedTextColor.DARK_RED)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, query.isBlank() + ? "Все активные баны." + : "Результаты поиска по фильтру: " + query), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openBanSearchDialog(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть banlist dialog: " + e.getMessage()); + return false; + } + } + + public boolean openBanDetailsDialog(Player player, String targetName) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ProfileBanList banList = Bukkit.getBanList(BanListType.PROFILE); + BanEntry entry = findBanEntry(banList, targetName); + if (entry == null) { + player.sendMessage(Component.text("§eБан для " + targetName + " уже снят.")); + schedule(() -> openBanListDialog(player)); + return true; + } + + String displayName = profileName(entry.getBanTarget()); + String targetIdentifier = profileIdentifier(entry.getBanTarget()); + List lines = new ArrayList<>(); + lines.add(bodyLine(player, "Игрок: " + displayName, false)); + lines.add(bodyLine(player, "Причина: " + blankFallback(entry.getReason(), "не указана"), true)); + lines.add(bodyLine(player, "Кем выдан: " + blankFallback(entry.getSource(), "неизвестно"), false)); + lines.add(bodyLine(player, "Выдан: " + formatDate(entry.getCreated()), false)); + lines.add(bodyLine(player, "Истекает: " + formatDate(entry.getExpiration()), false)); + + List actions = new ArrayList<>(); + if (player.hasPermission(PunishmentAction.UNBAN.permission)) { + actions.add(actionButton(player, "✅", "Разбанить", NamedTextColor.GREEN, + "Открыть подтверждение снятия бана", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> + openUnbanConfirmationDialog(m, targetIdentifier, displayName))))); + } + + DialogBase base = DialogBase.builder(title(player, "📄", "Бан: " + displayName, NamedTextColor.DARK_RED)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) @@ -308,217 +569,453 @@ public class AdminDialogService { .inputs(List.of()) .build(); - showDialog(player, base, DialogType.multiAction(List.of(), backButton, 1)); + showDialog(player, base, DialogType.multiAction(actions, + backButton(player, () -> openBanListDialog(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть детали бана " + targetName + ": " + e.getMessage()); + return false; + } + } + + private boolean openUnbanConfirmationDialog(Player moderator, String targetIdentifier, String displayName) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(PunishmentAction.UNBAN.permission)) { + return false; + } + try { + ActionButton confirmButton = actionButton(moderator, "✅", "Подтвердить разбан", NamedTextColor.GREEN, + "Удалить профиль из Paper BanList", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> { + if (!adminModeCommand.isInAdminMode(m.getUniqueId()) + || !m.hasPermission(PunishmentAction.UNBAN.permission)) { + m.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + BanEntry currentEntry = findBanEntry( + Bukkit.getBanList(BanListType.PROFILE), targetIdentifier); + if (currentEntry == null) { + m.sendMessage(configManager.getPrefixedMessage("§eБан для " + displayName + " уже снят.")); + } else { + currentEntry.remove(); + plugin.getLogger().info("UNBAN | Moderator: " + m.getName() + " | Player: " + displayName); + m.sendMessage(configManager.getPrefixedMessage("§aИгрок §f" + displayName + "§a разбанен.")); + } + openBanListDialog(m); + }))); + + DialogBase base = DialogBase.builder(title(moderator, "✅", "Разбанить: " + displayName, NamedTextColor.GREEN)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + "Подтвердите снятие активного бана из Paper BanList."), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(confirmButton), + backButton(moderator, () -> openBanDetailsDialog(moderator, targetIdentifier)), 1)); return true; } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть онлайн список: " + exception.getMessage()); + plugin.getLogger().warning("Не удалось открыть подтверждение разбана " + displayName + ": " + exception.getMessage()); + return false; + } + } + + public boolean openCoreProtectMenu(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, "🕵", "Inspector", NamedTextColor.AQUA, + "Переключить режим инспектора CoreProtect", false, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.toggleInspector(m); + schedule(() -> openCoreProtectMenu(m)); + }))); + buttons.add(actionButton(player, "🔎", "Lookup", NamedTextColor.GREEN, + "Поиск в логах CoreProtect", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "lookup"))))); + buttons.add(actionButton(player, "↩", "Rollback", NamedTextColor.RED, + "Откатить изменения по фильтрам", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "rollback"))))); + buttons.add(actionButton(player, "♻", "Restore", NamedTextColor.YELLOW, + "Вернуть изменения по фильтрам", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "restore"))))); + + DialogBase base = DialogBase.builder(title(player, "🧰", "CoreProtect", NamedTextColor.AQUA)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Полный набор действий CoreProtect: inspector, lookup, rollback, restore."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть меню CoreProtect: " + e.getMessage()); + return false; + } + } + + public boolean openCoreProtectActionDialog(Player player, String operation) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ActionButton confirmButton = actionButton(player, operation.equals("lookup") ? "🔎" : operation.equals("rollback") ? "↩" : "♻", + operation.equals("lookup") ? "Выполнить Lookup" : operation.equals("rollback") ? "Подтвердить Rollback" : "Подтвердить Restore", + operation.equals("lookup") ? NamedTextColor.GREEN : operation.equals("rollback") ? NamedTextColor.RED : NamedTextColor.YELLOW, + "Выполнить действие CoreProtect", + true, + (response, audience) -> withPlayer(audience, m -> { + String targetPlayer = normalize(response.getText("cp_player")); + String action = normalize(response.getText("cp_action")); + String block = normalize(response.getText("cp_block")); + String exclude = normalize(response.getText("cp_exclude")); + String world = normalize(response.getText("cp_world")); + String extra = normalize(response.getText("cp_extra")); + int radius = parseInt(response.getText("cp_radius"), configManager.getCoreProtectDefaultRadius()); + String time = normalizeTime(response.getText("cp_time"), configManager.getCoreProtectDefaultTime()); + + switch (operation) { + case "lookup" -> adminModeCommand.executeCPLookup(m, targetPlayer, action, block, exclude, world, radius, time); + case "rollback" -> adminModeCommand.executeCPRollback(m, radius, time, targetPlayer, action, block, exclude, world, extra); + case "restore" -> adminModeCommand.executeCPRestore(m, radius, time, targetPlayer, action, block, exclude, world, extra); + default -> { } + } + schedule(() -> openCoreProtectMenu(m)); + })); + + DialogBase base = DialogBase.builder(title(player, "🧰", "CoreProtect: " + operation.toUpperCase(), NamedTextColor.AQUA)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Фильтры: player, action, block, exclude, world и extra (через ;)."), 360))) + .inputs(List.of( + DialogInput.text("cp_player", 260, bodyText(player, "Игрок"), false, "", 32, null), + DialogInput.text("cp_action", 260, bodyText(player, "Action"), false, "", 48, null), + DialogInput.text("cp_block", 260, bodyText(player, "Block"), false, "", 64, null), + DialogInput.text("cp_exclude", 260, bodyText(player, "Exclude"), false, "", 64, null), + DialogInput.text("cp_world", 260, bodyText(player, "World"), false, "", 48, null), + DialogInput.text("cp_extra", 260, bodyText(player, "Extra filters (; separated)"), false, "", 128, null), + DialogInput.text("cp_radius", 180, bodyText(player, "Радиус"), true, + String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), + DialogInput.text("cp_time", 180, bodyText(player, "Время"), true, + configManager.getCoreProtectDefaultTime(), 8, null))) + .build(); + + showDialog(player, base, DialogType.multiAction(List.of(confirmButton), backButton(player, () -> openCoreProtectMenu(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог CoreProtect " + operation + ": " + e.getMessage()); + return false; + } + } + + public boolean openSettingsPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, player.getAllowFlight() ? "🕊" : "🪶", + player.getAllowFlight() ? "Полет: ВКЛ" : "Полет: ВЫКЛ", + player.getAllowFlight() ? NamedTextColor.GREEN : NamedTextColor.RED, + "Переключить полет", false, + (res, aud) -> withPlayer(aud, m -> { + m.setAllowFlight(!m.getAllowFlight()); + m.setFlying(m.getAllowFlight()); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, player.getGameMode() == GameMode.SURVIVAL ? "🧍" : "👁", + player.getGameMode() == GameMode.SURVIVAL ? "Режим: SURVIVAL" : "Режим: SPECTATOR", + player.getGameMode() == GameMode.SURVIVAL ? NamedTextColor.GREEN : NamedTextColor.AQUA, + "Переключить SURVIVAL/SPECTATOR", false, + (res, aud) -> withPlayer(aud, m -> { + m.setGameMode(m.getGameMode() == GameMode.SURVIVAL ? GameMode.SPECTATOR : GameMode.SURVIVAL); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, adminModeCommand.isVanished(player.getUniqueId()) ? "🫥" : "👁", + adminModeCommand.isVanished(player.getUniqueId()) ? "Vanish: ВКЛ" : "Vanish: ВЫКЛ", + adminModeCommand.isVanished(player.getUniqueId()) ? NamedTextColor.AQUA : NamedTextColor.GRAY, + "Переключить vanish", true, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.toggleVanish(m); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "🎛", "Скорости и бессмертие", NamedTextColor.LIGHT_PURPLE, + "Тонкая настройка режима", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openSpeedPanel(m))))); + + DialogBase base = DialogBase.builder(title(player, "⚙", "Настройки режима", NamedTextColor.GREEN)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Управление полетом, vanish, текущим режимом и боевыми параметрами."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть настройки режима: " + e.getMessage()); + return false; + } + } + + public boolean openPersonalSettingsPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ModeratorPreferencesStore.ModeratorPreferences preferences = preferencesStore.get(player.getUniqueId()); + + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, "🎮", + "Режим по умолчанию: " + preferences.defaultEntryMode().displayName(), + NamedTextColor.GREEN, + "Цикл: По серверу -> Survival -> Spectator", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleDefaultEntryMode(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "🎨", + "Тема: " + preferences.theme().displayName(), + NamedTextColor.LIGHT_PURPLE, + "Радужная, светлая акцентная или белая", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleTheme(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "💡", + "Акцент: " + preferences.accentColor().displayName(), + preferences.accentColor().textColor(), + "Цвет важных элементов для светлой темы", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleAccentColor(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "😀" : "□", + emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "Смайлики: ВКЛ" : "Смайлики: ВЫКЛ", + emojiPreferenceStore.isEnabled(player.getUniqueId()) ? NamedTextColor.YELLOW : NamedTextColor.GRAY, + "Включить или выключить смайлики в меню", false, + (res, aud) -> withPlayer(aud, m -> { + emojiPreferenceStore.toggle(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + + DialogBase base = DialogBase.builder(title(player, "🪪", "Личные настройки", NamedTextColor.LIGHT_PURPLE)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(List.of( + bodyLine(player, "Персональные настройки модератора сохраняются между перезаходами.", false), + bodyLine(player, "Текущая тема: " + preferences.theme().displayName(), true), + bodyLine(player, "Акцентный цвет: " + preferences.accentColor().displayName(), preferences.theme() == ModeratorPreferencesStore.MenuTheme.CLEAN_ACCENT), + bodyLine(player, "Смайлики: " + (emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "включены" : "выключены"), false))), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть личные настройки: " + e.getMessage()); return false; } } public boolean openSpeedPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { float currentFly = clamp(player.getFlySpeed(), 0.1f, 1.0f); float currentWalk = clamp(player.getWalkSpeed(), 0.1f, 1.0f); - ActionButton applyButton = actionButton( - "Применить", - "Сохранить выбранные значения скорости и бессмертия", - (response, audience) -> withPlayer(audience, moderator -> { + ActionButton applyButton = actionButton(player, "✅", "Применить", NamedTextColor.GREEN, + "Сохранить выбранные значения", true, + (response, audience) -> withPlayer(audience, m -> { Float flySpeed = response.getFloat("fly_speed"); Float walkSpeed = response.getFloat("walk_speed"); Boolean godMode = response.getBoolean("god_mode"); adminModeCommand.applyAdminSettings( - moderator, + m, flySpeed == null ? currentFly : flySpeed, walkSpeed == null ? currentWalk : walkSpeed, - godMode != null && godMode); - schedule(() -> openCurrentPanel(moderator)); + godMode == null || godMode); + schedule(() -> openSettingsPanel(m)); })); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в основную панель модератора", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); - - DialogBase base = DialogBase.builder(Component.text("🎛 Настройка режима")) + DialogBase base = DialogBase.builder(title(player, "🎛", "Скорости и бессмертие", NamedTextColor.LIGHT_PURPLE)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Настройте скорость полёта, скорость ходьбы и бессмертие для текущей сессии модерации."), - 360))) + .body(List.of(DialogBody.plainMessage(bodyText(player, "Настройте скорость полёта, ходьбы и бессмертие."), 360))) .inputs(List.of( - DialogInput.numberRange( - "fly_speed", - 320, - Component.text("Скорость полета"), - "%s: %s", - 0.1f, - 1.0f, - currentFly, - 0.05f), - DialogInput.numberRange( - "walk_speed", - 320, - Component.text("Скорость ходьбы"), - "%s: %s", - 0.1f, - 1.0f, - currentWalk, - 0.05f), - DialogInput.bool( - "god_mode", - Component.text("Бессмертие"), - player.isInvulnerable(), - "true", - "false"))) + DialogInput.numberRange("fly_speed", 320, bodyText(player, "Скорость полета"), + "%s: %s", 0.1f, 1.0f, currentFly, 0.05f), + DialogInput.numberRange("walk_speed", 320, bodyText(player, "Скорость ходьбы"), + "%s: %s", 0.1f, 1.0f, currentWalk, 0.05f), + DialogInput.bool("god_mode", bodyText(player, "Бессмертие"), + player.isInvulnerable(), "true", "false"))) .build(); - showDialog(player, base, DialogType.multiAction(List.of(applyButton), backButton, 1)); + showDialog(player, base, DialogType.multiAction(List.of(applyButton), backButton(player, () -> openSettingsPanel(player)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть dialog настройки скоростей: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть dialog настройки: " + e.getMessage()); return false; } } - public boolean openLookupDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { + private void addPunishmentActionButton(List buttons, Player moderator, Player target, + PunishmentAction action) { + String template = punishmentTemplate(action); + if (template == null || template.isBlank() || !moderator.hasPermission(action.permission)) { + return; + } + buttons.add(actionButton(moderator, action.emoji, action.label, action.color, action.tooltip, true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPunishmentActionDialog(m, target, action))))); + } + + private boolean openPunishmentActionDialog(Player moderator, Player target, PunishmentAction action) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(action.permission)) { return false; } - try { - ActionButton lookupButton = actionButton( - "🔍 Найти", - "Выполнить co lookup с указанными параметрами", - (response, audience) -> withPlayer(audience, moderator -> { - String targetName = normalizeTarget(response.getText("cp_player")); - Integer radius = parseInteger(response.getText("cp_radius"), configManager.getCoreProtectDefaultRadius()); - String time = normalizeTime(response.getText("cp_time"), configManager.getCoreProtectDefaultTime()); - adminModeCommand.executeCPLookup(moderator, targetName, radius, time); - schedule(() -> openHardPanel(moderator)); + ActionButton applyButton = actionButton(moderator, "✅", "Подтвердить", NamedTextColor.GREEN, + "Применить действие", true, + (response, audience) -> withPlayer(audience, m -> { + String reason = sanitizeReason(response.getText("reason")); + String duration = action.hasDuration + ? PunishmentDuration.normalizeToken(response.getText("punishment_duration")) + : ""; + schedule(() -> executePunishment(m, target, action, duration, reason)); })); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в Hard Moder panel", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openHardPanel(moderator)))); + List inputs = new ArrayList<>(); + if (action.hasDuration) { + String selected = defaultPunishmentDuration(action); + List options = configManager.getPunishmentDurationOptions().stream() + .map(token -> SingleOptionDialogInput.OptionEntry.create( + token, + bodyText(moderator, punishmentDurationLabel(token)), + token.equals(selected))) + .toList(); + inputs.add(DialogInput.singleOption("punishment_duration", 300, options, + bodyText(moderator, "Срок действия"), true)); + } + inputs.add(DialogInput.text("reason", 320, bodyText(moderator, "Причина"), false, "", 256, null)); - DialogBase base = DialogBase.builder(Component.text("🔍 Поиск в логах CP")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Фильтр по игроку опционален. Радиус и время можно оставить стандартными."), 360))) - .inputs(List.of( - DialogInput.text("cp_player", 300, Component.text("Игрок (пусто = все)"), false, "", 16, null), - DialogInput.text("cp_radius", 180, Component.text("Радиус"), true, - String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), - DialogInput.text("cp_time", 180, Component.text("Время"), true, - configManager.getCoreProtectDefaultTime(), 4, null))) + DialogBase base = DialogBase.builder(title(moderator, action.emoji, action.title, action.color)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + (action.hasDuration ? "Выберите срок и укажите причину для " : "Укажите причину для ") + + target.getName()), 360))) + .inputs(inputs) .build(); - showDialog(player, base, DialogType.multiAction(List.of(lookupButton), backButton, 1)); + showDialog(moderator, base, DialogType.multiAction(List.of(applyButton), + backButton(moderator, () -> openManagePlayerOrSearch(moderator, target)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть lookup dialog: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог действия " + action.title + ": " + e.getMessage()); return false; } } - public boolean openRollbackDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; + private void executePunishment(Player moderator, Player target, PunishmentAction action, String duration, String reason) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(action.permission)) { + moderator.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (!target.isOnline()) { + moderator.sendMessage(configManager.getPrefixedMessage("§eИгрок уже вышел с сервера. Действие отменено.")); + openPlayerSearchDialog(moderator); + return; + } + if (action.hasDuration && !configManager.isPunishmentDurationAllowed(duration)) { + moderator.sendMessage(configManager.getPrefixedMessage("§cВыбран неизвестный срок наказания.")); + openPunishmentActionDialog(moderator, target, action); + return; } - try { - ActionButton confirmButton = actionButton( - "✅ Далее к подтверждению", - "Открыть подтверждение rollback", - (response, audience) -> withPlayer(audience, moderator -> { - int radius = parseInteger(response.getText("rollback_radius"), configManager.getCoreProtectDefaultRadius()); - String time = normalizeTime(response.getText("rollback_time"), configManager.getCoreProtectDefaultTime()); - String targetPlayer = normalizeTarget(response.getText("rollback_player")); - schedule(() -> openRollbackConfirmDialog(moderator, radius, time, targetPlayer)); - })); + String sanitizedReason = reason == null || reason.isBlank() ? "-" : reason; + String command = punishmentTemplate(action) + .replace("{target}", target.getName()) + .replace("{moderator}", moderator.getName()) + .replace("{duration}", duration) + .replace("{reason}", sanitizedReason) + .trim(); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в Hard Moder panel", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openHardPanel(moderator)))); + boolean accepted = configManager.isModerationActionsAsConsole() + ? Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command) + : moderator.getServer().dispatchCommand(moderator, command); + if (!accepted) { + moderator.sendMessage(configManager.getPrefixedMessage( + "§cКоманда наказания не принята сервером. Warn/действие не сохранено.")); + plugin.getLogger().warning("Команда наказания отклонена: " + action.name() + " для " + target.getName()); + openManagePlayerOrSearch(moderator, target); + return; + } - DialogBase base = DialogBase.builder(Component.text("↩ Rollback CoreProtect")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Укажите радиус, временной диапазон и при необходимости игрока. Затем подтвердите откат."), 360))) - .inputs(List.of( - DialogInput.text("rollback_radius", 180, Component.text("Радиус"), true, - String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), - DialogInput.text("rollback_time", 180, Component.text("Время"), true, - configManager.getCoreProtectDefaultTime(), 4, null), - DialogInput.text("rollback_player", 300, Component.text("Игрок (опционально)"), false, - "", 16, null))) - .build(); - - showDialog(player, base, DialogType.multiAction(List.of(confirmButton), backButton, 1)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть rollback dialog: " + exception.getMessage()); - return false; + if (action == PunishmentAction.WARN) { + warningStore.recordWarn(target, moderator, sanitizedReason, duration); + } + moderator.sendMessage(configManager.getPrefixedMessage("§aДействие " + action.label + " применено к §f" + target.getName() + "§a.")); + if (action == PunishmentAction.BAN || action == PunishmentAction.KICK) { + openPlayerSearchDialog(moderator); + } else { + openManagePlayerOrSearch(moderator, target); } } - public boolean openRollbackConfirmDialog(Player player, int radius, String time, String targetPlayer) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } + private boolean matchesQuery(String value, String query) { + return query == null || query.isBlank() || (value != null && value.toLowerCase().contains(query.toLowerCase())); + } - try { - List lines = new ArrayList<>(); - lines.add(Component.text("Радиус: " + radius + " блоков")); - lines.add(Component.text("Время: " + time)); - lines.add(Component.text(targetPlayer.isEmpty() ? "Игрок: все" : "Игрок: " + targetPlayer)); - - ActionButton confirmButton = actionButton( - "✅ Откатить", - "Подтвердить rollback по указанным параметрам", - (response, audience) -> withPlayer(audience, moderator -> { - adminModeCommand.executeCPRollback(moderator, radius, time, targetPlayer); - schedule(() -> openHardPanel(moderator)); - })); - ActionButton cancelButton = actionButton( - "❌ Отмена", - "Отменить rollback и вернуться назад", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openRollbackDialog(moderator)))); - - DialogBase base = DialogBase.builder(Component.text("⚠ Подтвердить откат?")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(List.of(confirmButton, cancelButton), closeButton(), 1)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть подтверждение rollback: " + exception.getMessage()); - return false; + private void openManagePlayerOrSearch(Player moderator, Player target) { + if (target != null && target.isOnline()) { + openManagePlayerDialog(moderator, target); + } else { + openPlayerSearchDialog(moderator); } } - private void showDialog(Player player, DialogBase base, io.papermc.paper.registry.data.dialog.type.DialogType type) { + private String punishmentTemplate(PunishmentAction action) { + return switch (action) { + case WARN -> configManager.getWarnCommandTemplate(); + case MUTE -> configManager.getMuteCommandTemplate(); + case UNMUTE -> configManager.getUnmuteCommandTemplate(); + case KICK -> configManager.getKickCommandTemplate(); + case BAN -> configManager.getBanCommandTemplate(); + case UNWARN, UNBAN -> ""; + }; + } + + private String defaultPunishmentDuration(PunishmentAction action) { + return switch (action) { + case WARN -> configManager.getDefaultWarnDuration(); + case MUTE -> configManager.getDefaultMuteDuration(); + case BAN -> configManager.getDefaultBanDuration(); + case KICK, UNMUTE, UNWARN, UNBAN -> ""; + }; + } + + private String punishmentDurationLabel(String token) { + return switch (token) { + case "1h" -> "1 час"; + case "1d" -> "1 день"; + case "7d" -> "7 дней"; + case "30d" -> "30 дней"; + case "permanent" -> "Permanently"; + default -> token; + }; + } + + private ActionButton backButton(Player player, Runnable callback) { + return actionButton(player, "↩", "Назад", NamedTextColor.GRAY, "Вернуться назад", true, + (res, aud) -> withPlayer(aud, m -> schedule(callback))); + } + + private void showDialog(Player player, DialogBase base, DialogType type) { player.closeInventory(); Dialog dialog = Dialog.create(factory -> { var builder = factory.empty(); @@ -528,32 +1025,74 @@ public class AdminDialogService { player.showDialog(dialog); } - private ActionButton actionButton(String label, String tooltip, DialogActionCallback callback) { - return ActionButton.builder(Component.text(label)) - .tooltip(Component.text(tooltip)) + private ActionButton actionButton(Player player, String emoji, String text, NamedTextColor color, String tooltip, + boolean important, DialogActionCallback callback) { + return actionButton(label(player, emoji, text, color, important), bodyText(player, tooltip), callback); + } + + private ActionButton actionButton(Component label, Component tooltip, DialogActionCallback callback) { + return ActionButton.builder(label) + .tooltip(tooltip) .width(170) .action(DialogAction.customClick(callback, CALLBACK_OPTIONS)) .build(); } - private ActionButton closeButton() { - return ActionButton.builder(Component.text("Закрыть")) - .width(140) - .build(); + private ActionButton closeButton(Player player) { + return ActionButton.builder(label(player, "✖", "Закрыть", NamedTextColor.GRAY, false)).width(140).build(); + } + + private Component title(Player player, String emoji, String text, NamedTextColor defaultColor) { + return label(player, emoji, text, defaultColor, true); + } + + private Component label(Player player, String emoji, String text, NamedTextColor color, boolean important) { + return Component.text(renderLabel(player, emoji, text), themedColor(player, color, important)); + } + + private Component playerLabel(Player player, String text, NamedTextColor color, boolean important) { + return Component.text(text, themedColor(player, color, important)); + } + + private Component bodyText(Player player, String text) { + return bodyText(player, text, false); + } + + private Component bodyText(Player player, String text, boolean important) { + return Component.text(text, themedColor(player, NamedTextColor.WHITE, important)); + } + + private Component bodyLine(Player player, String text, boolean important) { + return bodyText(player, text, important); + } + + private NamedTextColor themedColor(Player player, NamedTextColor defaultColor, boolean important) { + if (player == null) { + return defaultColor; + } + ModeratorPreferencesStore.ModeratorPreferences preferences = preferencesStore.get(player.getUniqueId()); + return switch (preferences.theme()) { + case RAINBOW -> defaultColor; + case CLEAN_ACCENT -> important ? preferences.accentColor().textColor() : NamedTextColor.WHITE; + case WHITE -> NamedTextColor.WHITE; + }; + } + + private String renderLabel(Player player, String emoji, String text) { + if (player != null && !emojiPreferenceStore.isEnabled(player.getUniqueId())) { + return text; + } + return emoji + " " + text; } private void withPlayer(net.kyori.adventure.audience.Audience audience, java.util.function.Consumer consumer) { - if (audience instanceof Player player) { - consumer.accept(player); - } + if (audience instanceof Player player) consumer.accept(player); } private Component joinLines(List lines) { Component result = Component.empty(); for (int i = 0; i < lines.size(); i++) { - if (i > 0) { - result = result.append(Component.newline()); - } + if (i > 0) result = result.append(Component.newline()); result = result.append(lines.get(i)); } return result; @@ -563,51 +1102,143 @@ public class AdminDialogService { plugin.getServer().getScheduler().runTask(plugin, runnable); } - private String normalizeTarget(String targetName) { - return targetName == null ? "" : targetName.trim(); + private String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private String sanitizeReason(String reason) { + if (reason == null) { + return ""; + } + return reason.replace('\n', ' ').replace('\r', ' ').trim(); + } + + private String buildBanSummary(BanEntry entry) { + String reason = blankFallback(entry.getReason(), "без причины"); + if (reason.length() > 48) { + reason = reason.substring(0, 45) + "..."; + } + return "Причина: " + reason + " | До: " + formatDate(entry.getExpiration()); + } + + private boolean isExpiredBan(BanEntry entry) { + return entry.getExpiration() != null && !entry.getExpiration().after(new Date()); + } + + private BanEntry findBanEntry(ProfileBanList banList, String identifier) { + for (BanEntry entry : banList.>getEntries()) { + PlayerProfile profile = entry.getBanTarget(); + if (profileIdentifier(profile).equalsIgnoreCase(identifier) + || profileName(profile).equalsIgnoreCase(identifier)) { + return entry; + } + } + return null; + } + + private String profileIdentifier(PlayerProfile profile) { + if (profile.getId() != null) { + return profile.getId().toString(); + } + return profileName(profile); + } + + private String profileName(PlayerProfile profile) { + return blankFallback(profile.getName(), + profile.getId() == null ? "неизвестно" : profile.getId().toString()); + } + + private String formatWarnInline(ModeratorWarningStore.WarningEntry warning) { + return formatTimestamp(warning.createdAt()) + " | " + warningStatusLabel(warning) + " | " + warning.reason(); + } + + private String formatWarnHeader(ModeratorWarningStore.WarningEntry warning) { + return formatTimestamp(warning.createdAt()) + " • " + warningStatusLabel(warning) + " • " + warning.moderatorName(); + } + + private String warningStatusLabel(ModeratorWarningStore.WarningEntry warning) { + return switch (warning.status()) { + case ACTIVE -> "активен"; + case EXPIRED -> "истёк"; + case REVOKED -> "снят"; + }; + } + + private String formatWarnExpiration(ModeratorWarningStore.WarningEntry warning) { + return warning.isPermanent() ? "Permanently" : formatTimestamp(warning.expiresAt()); + } + + private String formatOptionalTimestamp(long timestamp) { + return timestamp <= 0L ? "данные отсутствуют" : formatTimestamp(timestamp); + } + + private String blankFallback(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } + + private String formatDate(Date date) { + if (date == null) { + return "никогда"; + } + return DATE_FORMATTER.format(date.toInstant()); + } + + private String formatTimestamp(long timestamp) { + return DATE_FORMATTER.format(Instant.ofEpochMilli(timestamp)); } private String normalizeTime(String input, String fallback) { - if (input == null || input.isBlank()) { - return fallback; - } - + if (input == null || input.isBlank()) return fallback; String normalized = input.trim().toLowerCase(); - if (normalized.matches("\\d+[smhdw]")) { - return normalized; - } - return fallback; + return normalized.matches("\\d+[smhdw]") ? normalized : fallback; } - private Integer parseInteger(String value, int fallback) { - if (value == null || value.isBlank()) { - return fallback; - } - + private int parseInt(String value, int fallback) { + if (value == null || value.isBlank()) return fallback; try { return Integer.parseInt(value.trim()); - } catch (NumberFormatException exception) { + } catch (NumberFormatException e) { return fallback; } } - private String dialogTitleForAction(PlayerListAction action) { - return switch (action) { - case SPECTATE -> "👁 Spectate игрока"; - case TELEPORT -> "📍 TP к игроку"; - case INVSEE -> "🎒 Invsee игрока"; - }; - } - - private String dialogBodyForAction(PlayerListAction action) { - return switch (action) { - case SPECTATE -> "Выберите игрока, за которым хотите наблюдать."; - case TELEPORT -> "Выберите игрока, к которому хотите телепортироваться."; - case INVSEE -> "Выберите игрока, чей инвентарь хотите открыть."; - }; - } - private float clamp(float value, float min, float max) { return Math.max(min, Math.min(max, value)); } + + private enum PunishmentAction { + WARN("⚠", "Warn", "Предупреждение", "Выдать предупреждение", NamedTextColor.YELLOW, + "adminmode.punishment.warn", true), + MUTE("🛑", "Mute", "Мут", "Выдать мут", NamedTextColor.GOLD, + "adminmode.punishment.mute", true), + UNMUTE("🔊", "Unmute", "Снять мут", "Снять активный мут", NamedTextColor.GREEN, + "adminmode.punishment.unmute", false), + KICK("👢", "Kick", "Кик", "Кикнуть игрока", NamedTextColor.RED, + "adminmode.punishment.kick", false), + BAN("🔨", "Ban", "Бан", "Забанить игрока", NamedTextColor.DARK_RED, + "adminmode.punishment.ban", true), + UNWARN("✅", "Unwarn", "Снять warn", "Снять предупреждение", NamedTextColor.GREEN, + "adminmode.punishment.unwarn", false), + UNBAN("✅", "Unban", "Разбан", "Снять бан", NamedTextColor.GREEN, + "adminmode.punishment.unban", false); + + private final String emoji; + private final String label; + private final String title; + private final String tooltip; + private final NamedTextColor color; + private final String permission; + private final boolean hasDuration; + + PunishmentAction(String emoji, String label, String title, String tooltip, NamedTextColor color, + String permission, boolean hasDuration) { + this.emoji = emoji; + this.label = label; + this.title = title; + this.tooltip = tooltip; + this.color = color; + this.permission = permission; + this.hasDuration = hasDuration; + } + } } diff --git a/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java b/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java new file mode 100755 index 0000000..0862fc5 --- /dev/null +++ b/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java @@ -0,0 +1,63 @@ +package org.blz.adminmode.dialog; + +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Persists which players have personally enabled the Dialog-based admin panel. + * Stored in /dialog_prefs.dat (one UUID per line). + * Default: disabled for every player until they explicitly toggle on. + */ +public class DialogPreferenceStore { + + private final File file; + private final Set enabledPlayers = new HashSet<>(); + + public DialogPreferenceStore(Plugin plugin) { + file = new File(plugin.getDataFolder(), "dialog_prefs.dat"); + load(); + } + + public boolean isEnabled(UUID playerId) { + return enabledPlayers.contains(playerId); + } + + /** Toggles the preference and persists. Returns the new state (true = enabled). */ + public boolean toggle(UUID playerId) { + if (enabledPlayers.remove(playerId)) { + save(); + return false; + } + enabledPlayers.add(playerId); + save(); + return true; + } + + private void load() { + if (!file.exists()) return; + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + try { + enabledPlayers.add(UUID.fromString(line)); + } catch (IllegalArgumentException ignored) {} + } + } catch (IOException ignored) {} + } + + private void save() { + file.getParentFile().mkdirs(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (UUID uuid : enabledPlayers) { + writer.write(uuid.toString()); + writer.newLine(); + } + } catch (IOException ignored) {} + } +} diff --git a/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java b/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java new file mode 100755 index 0000000..809c5dd --- /dev/null +++ b/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java @@ -0,0 +1,72 @@ +package org.blz.adminmode.dialog; + +import org.bukkit.plugin.Plugin; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Stores players who disabled emojis in the moderation dialogs. + * Default: emojis are enabled. + */ +public class MenuEmojiPreferenceStore { + + private final File file; + private final Set disabledPlayers = new HashSet<>(); + + public MenuEmojiPreferenceStore(Plugin plugin) { + this.file = new File(plugin.getDataFolder(), "menu_emoji_prefs.dat"); + load(); + } + + public boolean isEnabled(UUID playerId) { + return !disabledPlayers.contains(playerId); + } + + public boolean toggle(UUID playerId) { + if (disabledPlayers.remove(playerId)) { + save(); + return true; + } + disabledPlayers.add(playerId); + save(); + return false; + } + + private void load() { + if (!file.exists()) return; + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + try { + disabledPlayers.add(UUID.fromString(line)); + } catch (IllegalArgumentException ignored) { + } + } + } catch (IOException ignored) { + } + } + + private void save() { + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (UUID uuid : disabledPlayers) { + writer.write(uuid.toString()); + writer.newLine(); + } + } catch (IOException ignored) { + } + } +} diff --git a/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java b/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java old mode 100644 new mode 100755 index b86a659..b446ca8 --- a/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java +++ b/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java @@ -29,31 +29,67 @@ public class CoreProtectIntegration { } public boolean lookup(Player player, String targetPlayer, int radius, String time) { + return lookup(player, targetPlayer, "", "", "", "", radius, time); + } + + public boolean lookup(Player player, String targetPlayer, String action, String block, String exclude, + String world, int radius, String time) { if (!isAvailable()) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); return false; } StringBuilder command = new StringBuilder("co lookup"); - appendPlayerFilter(command, targetPlayer); - command.append(" r:").append(normalizeRadius(radius)); - command.append(" t:").append(normalizeTime(time)); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); return Bukkit.dispatchCommand(player, command.toString()); } - public boolean rollback(Player player, int radius, String time, String targetPlayer) { + public boolean rollback(Player player, int radius, String time, String targetPlayer, String action, String block, String exclude) { + return rollback(player, radius, time, targetPlayer, action, block, exclude, "", ""); + } + + public boolean rollback(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { if (!isAvailable()) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); return false; } StringBuilder command = new StringBuilder("co rollback"); - appendPlayerFilter(command, targetPlayer); - command.append(" r:").append(normalizeRadius(radius)); - command.append(" t:").append(normalizeTime(time)); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); + appendExtraFilters(command, extraFilters); return Bukkit.dispatchCommand(player, command.toString()); } + public boolean restore(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!isAvailable()) { + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); + return false; + } + + StringBuilder command = new StringBuilder("co restore"); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); + appendExtraFilters(command, extraFilters); + return Bukkit.dispatchCommand(player, command.toString()); + } + + private String sanitizeAction(String action) { + String normalized = action.trim(); + if (normalized.matches("[a-zA-Z0-9_,:+\\-]+")) { + return normalized; + } + return ""; + } + + private String sanitizeBlock(String block) { + String normalized = block.trim(); + if (normalized.matches("[a-zA-Z0-9_,:]+")) { + return normalized; + } + return ""; + } + private void appendPlayerFilter(StringBuilder command, String targetPlayer) { String sanitized = sanitizePlayerName(targetPlayer); if (!sanitized.isEmpty()) { @@ -61,6 +97,58 @@ public class CoreProtectIntegration { } } + private void appendActionFilter(StringBuilder command, String action) { + String sanitized = sanitizeAction(action); + if (!sanitized.isEmpty()) { + command.append(" a:").append(sanitized); + } + } + + private void appendBlockFilter(StringBuilder command, String block) { + String sanitized = sanitizeBlock(block); + if (!sanitized.isEmpty()) { + command.append(" b:").append(sanitized); + } + } + + private void appendExcludeFilter(StringBuilder command, String exclude) { + String sanitized = sanitizeBlock(exclude); + if (!sanitized.isEmpty()) { + command.append(" e:").append(sanitized); + } + } + + private void appendWorldFilter(StringBuilder command, String world) { + String sanitized = sanitizeWorld(world); + if (!sanitized.isEmpty()) { + command.append(" w:").append(sanitized); + } + } + + private void appendCommonFilters(StringBuilder command, String targetPlayer, String action, String block, + String exclude, String world, int radius, String time) { + appendPlayerFilter(command, targetPlayer); + appendActionFilter(command, action); + appendBlockFilter(command, block); + appendExcludeFilter(command, exclude); + appendWorldFilter(command, world); + command.append(" r:").append(normalizeRadius(radius)); + command.append(" t:").append(normalizeTime(time)); + } + + private void appendExtraFilters(StringBuilder command, String extraFilters) { + if (extraFilters == null || extraFilters.isBlank()) { + return; + } + String[] tokens = extraFilters.split(";"); + for (String token : tokens) { + String normalized = token.trim(); + if (normalized.matches("[a-zA-Z]+:[A-Za-z0-9_.,:+\\-]+")) { + command.append(' ').append(normalized); + } + } + } + private int normalizeRadius(int radius) { return Math.max(1, Math.min(100, radius)); } @@ -89,4 +177,15 @@ public class CoreProtectIntegration { } return ""; } + + private String sanitizeWorld(String world) { + if (world == null || world.isBlank()) { + return ""; + } + String normalized = world.trim(); + if (normalized.matches("[A-Za-z0-9_:\\-]+")) { + return normalized; + } + return ""; + } } diff --git a/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java b/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java old mode 100644 new mode 100755 index 48f4dc4..833929a --- a/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java +++ b/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java @@ -6,7 +6,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.node.Node; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.moderation.ModeSession; -import org.blz.adminmode.moderation.ModeratorProfile; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; @@ -21,64 +20,54 @@ public class LuckPermsIntegration { } public String resolveCurrentGroup(Player player) { - if (!configManager.isUseLuckPerms()) { - return configManager.getBaseGroup(); - } - + if (!configManager.isUseLuckPerms()) return configManager.getBaseGroup(); try { User user = resolveUser(player); - if (user == null) { - return configManager.getBaseGroup(); - } - return user.getPrimaryGroup(); - } catch (IllegalStateException exception) { + return user != null ? user.getPrimaryGroup() : configManager.getBaseGroup(); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось определить текущую группу LuckPerms для " + player.getName() + ": " + e.getMessage()); return configManager.getBaseGroup(); } } - public void enterMode(Player player, ModeratorProfile profile) { + public void enterMode(Player player) { if (!configManager.isUseLuckPerms()) { - player.setOp(true); + plugin.getLogger().warning("LuckPerms-интеграция отключена в конфиге. Временные группы для " + player.getName() + " не применены."); return; } - try { User user = resolveUser(player); - if (user == null) { - return; + if (user == null) return; + for (String group : configManager.getLuckPermsGroups()) { + user.data().add(groupNode(group)); } - - removeKnownGroups(user); - user.data().add(groupNode(configManager.getActiveGroup(profile))); saveUser(user); - } catch (IllegalStateException exception) { - player.setOp(true); - plugin.getLogger().warning("LuckPerms не найден, используется OP для " + player.getName()); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось выдать временные группы LuckPerms для " + player.getName() + ": " + e.getMessage()); } } public void exitMode(Player player, ModeSession session) { if (!configManager.isUseLuckPerms()) { - player.setOp(false); + plugin.getLogger().warning("LuckPerms-интеграция отключена в конфиге. Группы для " + player.getName() + " не менялись."); return; } - try { User user = resolveUser(player); - if (user == null) { - return; + if (user == null) return; + if (configManager.isRemoveGroupsOnDisable()) { + removeTemporaryGroups(user); } - - removeKnownGroups(user); - String restoreGroup = session.getPreviousGroup(); - if (restoreGroup == null || restoreGroup.isBlank() || "op".equalsIgnoreCase(restoreGroup)) { - restoreGroup = configManager.getBaseGroup(); - } - user.data().add(groupNode(restoreGroup)); saveUser(user); - } catch (IllegalStateException exception) { - player.setOp(false); - plugin.getLogger().warning("LuckPerms не найден, снят OP у " + player.getName()); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось восстановить группы LuckPerms для " + player.getName() + ": " + e.getMessage()); + } + } + + private void removeTemporaryGroups(User user) { + user.data().remove(groupNode(configManager.getAdminGroup())); + for (String group : configManager.getLuckPermsGroups()) { + user.data().remove(groupNode(group)); } } @@ -91,12 +80,6 @@ public class LuckPermsIntegration { return user; } - private void removeKnownGroups(User user) { - user.data().remove(groupNode(configManager.getBaseGroup())); - user.data().remove(groupNode(configManager.getSoftGroup())); - user.data().remove(groupNode(configManager.getHardGroup())); - } - private Node groupNode(String group) { return Node.builder("group." + group).build(); } diff --git a/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java b/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java old mode 100644 new mode 100755 index 535c5d0..5b64824 --- a/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java +++ b/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java @@ -97,21 +97,14 @@ public class AdminModeListener implements Listener { if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; - if ((e.getAction() == Action.RIGHT_CLICK_AIR || e.getAction() == Action.RIGHT_CLICK_BLOCK) - && e.getItem() != null - && modeManager.isPanelItem(e.getItem())) { + if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.getClickedBlock() != null + && e.getClickedBlock().getType() == Material.ENDER_CHEST) { e.setCancelled(true); - - String panelType = modeManager.getPanelType(e.getItem()); - if (ModeManager.PANEL_CP_INSPECTOR.equals(panelType)) { - adminModeCommand.toggleInspector(player); - return; - } - - dialogService.openCurrentPanel(player); + player.sendMessage(configManager.getPrefixedMessage("§cВ admin mode нельзя взаимодействовать с эндер-сундуком.")); return; } + if (!configManager.isPreventItemUse()) return; if (e.getAction() == Action.RIGHT_CLICK_BLOCK) { diff --git a/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java b/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java old mode 100644 new mode 100755 index da3d7b2..f27a296 --- a/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java +++ b/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java @@ -4,8 +4,6 @@ import java.util.List; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; -import org.blz.adminmode.moderation.ModeratorProfile; -import org.bukkit.GameMode; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -26,29 +24,11 @@ public class GameModeChangeListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onGameModeChange(PlayerGameModeChangeEvent event) { Player player = event.getPlayer(); - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return; - } - - ModeratorProfile activeProfile = adminModeCommand.getActiveProfile(player.getUniqueId()); - boolean hardProfile = activeProfile != null && activeProfile.isHardProfile(); - - if (event.getNewGameMode() == GameMode.CREATIVE && !hardProfile) { - event.setCancelled(true); - player.sendMessage(configManager.getPrefixedMessage("§c✖ В режиме Soft Moder креатив запрещен!")); - return; - } - - if (event.getNewGameMode() == GameMode.CREATIVE && hardProfile) { - return; - } + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; List allowedModes = configManager.getAllowedGameModes(); - if (!allowedModes.isEmpty()) { String newMode = event.getNewGameMode().name(); - if (!allowedModes.contains(newMode)) { event.setCancelled(true); player.sendMessage(configManager.getPrefixedMessage("§c✖ В админ моде можно переключаться только на разрешенные режимы!")); @@ -57,15 +37,18 @@ public class GameModeChangeListener implements Listener { } } - // Включаем полет после смены режима, если это разрешено + if (event.getNewGameMode() == org.bukkit.GameMode.CREATIVE && !player.hasPermission("adminmode.chief")) { + event.setCancelled(true); + player.sendMessage(configManager.getPrefixedMessage("§cКреатив в admin mode доступен только главным модераторам.")); + return; + } + if (configManager.isAllowFlightInSurvival()) { player.getServer().getScheduler().runTaskLater( player.getServer().getPluginManager().getPlugin("AdminMode"), () -> { player.setAllowFlight(true); - if (!player.isFlying()) { - player.setFlying(true); - } + if (!player.isFlying()) player.setFlying(true); }, 1L); } @@ -74,18 +57,8 @@ public class GameModeChangeListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onToggleFlight(PlayerToggleFlightEvent event) { Player player = event.getPlayer(); - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return; - } - - if (!configManager.isAllowFlightInSurvival()) { - return; - } - - // Всегда разрешаем полет в админ моде - if (!player.getAllowFlight()) { - player.setAllowFlight(true); - } + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; + if (!configManager.isAllowFlightInSurvival()) return; + if (!player.getAllowFlight()) player.setAllowFlight(true); } } diff --git a/src/main/java/org/blz/adminmode/listeners/MobTargetListener.java b/src/main/java/org/blz/adminmode/listeners/MobTargetListener.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java b/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java old mode 100644 new mode 100755 index 5d60b65..8088e2b --- a/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java +++ b/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java @@ -15,6 +15,6 @@ public class PlayerDeathListener implements Listener { @EventHandler public void onPlayerDeath(PlayerDeathEvent event) { - modeManager.handleHardModeDeath(event); + modeManager.handleAdminDeath(event); } } diff --git a/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java b/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java old mode 100644 new mode 100755 index b353cc5..bf8ee4c --- a/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java +++ b/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java @@ -26,13 +26,14 @@ public class PlayerSessionListener implements Listener { @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); + modeManager.refreshVanishFor(player); if (!modeManager.hasSession(player.getUniqueId())) { return; } player.getServer().getScheduler().runTaskLater( org.blz.adminmode.AdminMode.getInstance(), - () -> adminModeCommand.recoverSession(player), + () -> adminModeCommand.resumeMode(player), 1L); } } diff --git a/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java b/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java old mode 100644 new mode 100755 index 9b33ad7..e09e5ae --- a/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java +++ b/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java @@ -10,6 +10,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; @@ -23,6 +24,7 @@ import java.util.*; public class BelzeBoolManager implements Listener { + private static final String SPECIAL_PERMISSION = "adminmode.special.belzebool"; private final Plugin plugin; private final String BELZEBOOL_NAME = "BelzeBool"; private final String POCKET_DIMENSION_WORLD = "pocket_dimension"; @@ -39,6 +41,9 @@ public class BelzeBoolManager implements Listener { // Vanish State: UUID -> isVanished private final Set vanishedPlayers = new HashSet<>(); + // Pocket return state: player UUID -> original location before abduction + private final Map pocketReturnLocations = new HashMap<>(); + // Input Tracking private final Map> rightClickTimestamps = new HashMap<>(); @@ -49,7 +54,7 @@ public class BelzeBoolManager implements Listener { } public boolean isBelzeBool(Player player) { - return player.getName().equalsIgnoreCase(BELZEBOOL_NAME); + return player.hasPermission(SPECIAL_PERMISSION) && player.getName().equalsIgnoreCase(BELZEBOOL_NAME); } // --- Casting Mode Logic --- @@ -278,6 +283,9 @@ public class BelzeBoolManager implements Listener { target.getWorld().spawnParticle(Particle.SCULK_SOUL, target.getLocation(), 50, 0.5, 1, 0.5, 0.1); Location originalLoc = target.getLocation(); + if (target instanceof Player targetPlayer) { + pocketReturnLocations.put(targetPlayer.getUniqueId(), originalLoc.clone()); + } // 2. Teleport after 5 seconds new BukkitRunnable() { @@ -320,6 +328,8 @@ public class BelzeBoolManager implements Listener { if (target instanceof Player targetP) { targetP.removePotionEffect(PotionEffectType.LEVITATION); targetP.removePotionEffect(PotionEffectType.DARKNESS); + targetP.removePotionEffect(PotionEffectType.SLOWNESS); + pocketReturnLocations.remove(targetP.getUniqueId()); } } } @@ -410,4 +420,52 @@ public class BelzeBoolManager implements Listener { player.sendMessage(Component.text("You have vanished.", NamedTextColor.AQUA)); } } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Location returnLocation = pocketReturnLocations.remove(player.getUniqueId()); + if (returnLocation == null) { + return; + } + + Bukkit.getScheduler().runTask(plugin, () -> { + player.teleport(returnLocation); + player.removePotionEffect(PotionEffectType.LEVITATION); + player.removePotionEffect(PotionEffectType.DARKNESS); + player.removePotionEffect(PotionEffectType.SLOWNESS); + }); + } + + public boolean forceDisable(Player player) { + boolean changed = false; + UUID uuid = player.getUniqueId(); + + if (castingModePlayers.remove(uuid)) { + changed = true; + } + rightClickTimestamps.remove(uuid); + selectedSpellIndex.remove(uuid); + cooldowns.remove(uuid); + + if (vanishedPlayers.remove(uuid)) { + for (Player online : Bukkit.getOnlinePlayers()) { + online.showPlayer(plugin, player); + } + changed = true; + } + + Location returnLocation = pocketReturnLocations.remove(uuid); + if (returnLocation != null) { + changed = true; + if (player.isOnline()) { + player.teleport(returnLocation); + } + player.removePotionEffect(PotionEffectType.LEVITATION); + player.removePotionEffect(PotionEffectType.DARKNESS); + player.removePotionEffect(PotionEffectType.SLOWNESS); + } + + return changed; + } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeManager.java b/src/main/java/org/blz/adminmode/moderation/ModeManager.java old mode 100644 new mode 100755 index 79f00b5..92ffa6d --- a/src/main/java/org/blz/adminmode/moderation/ModeManager.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeManager.java @@ -8,7 +8,6 @@ import org.blz.adminmode.session.SessionStorage; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.attribute.Attribute; import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; @@ -16,46 +15,44 @@ import org.bukkit.entity.Player; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; public class ModeManager { - public static final String PANEL_SOFT = "soft"; - public static final String PANEL_HARD = "hard"; - public static final String PANEL_CP_INSPECTOR = "cp_inspector"; - private final Plugin plugin; private final ConfigManager configManager; private final LuckPermsIntegration luckPermsIntegration; private final SessionStorage sessionStorage; private final ModeSessionLogger sessionLogger; - private final NamespacedKey panelTypeKey; + private final ModeratorPreferencesStore preferencesStore; private final Map activeSessions = new HashMap<>(); + private final Set vanishedModerators = new HashSet<>(); public ModeManager( Plugin plugin, ConfigManager configManager, LuckPermsIntegration luckPermsIntegration, SessionStorage sessionStorage, - ModeSessionLogger sessionLogger) { + ModeSessionLogger sessionLogger, + ModeratorPreferencesStore preferencesStore) { this.plugin = plugin; this.configManager = configManager; this.luckPermsIntegration = luckPermsIntegration; this.sessionStorage = sessionStorage; this.sessionLogger = sessionLogger; - this.panelTypeKey = new NamespacedKey(plugin, "panel_type"); + this.preferencesStore = preferencesStore; this.activeSessions.putAll(sessionStorage.loadSessions()); } @@ -71,37 +68,32 @@ public class ModeManager { return player == null ? null : getSession(player.getUniqueId()); } - public ModeratorProfile getProfile(UUID playerId) { - ModeSession session = getSession(playerId); - return session == null ? null : session.getProfile(); - } - - public List getAvailableProfiles(Player player) { - return ModeratorProfile.availableFor(player); - } - public Collection getActiveSessions() { return new ArrayList<>(activeSessions.values()); } - public boolean enterMode(Player player, ModeratorProfile profile) { - if (player == null || profile == null || hasSession(player.getUniqueId())) { - return false; - } + public boolean enterMode(Player player) { + if (player == null || hasSession(player.getUniqueId())) return false; + if (!player.hasPermission(ModeratorProfile.ADMIN.getPermission())) return false; String previousGroup = luckPermsIntegration.resolveCurrentGroup(player); - String activeGroup = configManager.getActiveGroup(profile); - ModeSession session = ModeSession.capture(player, profile, previousGroup, activeGroup); - activeSessions.put(player.getUniqueId(), session); - persistSessions(); + ModeSession session = ModeSession.capture(player, ModeratorProfile.ADMIN, previousGroup, configManager.getAdminGroup()); + try { + activeSessions.put(player.getUniqueId(), session); + persistSessions(); - preparePlayerForModeration(player, profile); - luckPermsIntegration.enterMode(player, profile); - sessionLogger.logEnter(session); + preparePlayerForModeration(player); + luckPermsIntegration.enterMode(player); + sessionLogger.logEnter(session); - String enterMessage = profile.isHardProfile() ? configManager.getMsgEnterHard() : configManager.getMsgEnterSoft(); - player.sendMessage(configManager.getPrefixedMessage(enterMessage)); - return true; + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgEnter())); + return true; + } catch (RuntimeException exception) { + activeSessions.remove(player.getUniqueId()); + persistSessions(); + plugin.getLogger().warning("AdminMode: не удалось включить режим для " + player.getName() + ": " + exception.getMessage()); + return false; + } } public boolean exitMode(Player player) { @@ -112,6 +104,19 @@ public class ModeManager { return finishSession(player, true); } + public boolean resumeMode(Player player) { + if (player == null || !hasSession(player.getUniqueId())) { + return false; + } + + applyResumedModerationState(player); + luckPermsIntegration.enterMode(player); + if (isVanished(player.getUniqueId())) { + reapplyVanish(player); + } + return true; + } + public void markQuit(Player player) { if (player != null && hasSession(player.getUniqueId())) { persistSessions(); @@ -119,33 +124,48 @@ public class ModeManager { } public boolean canUseHardActions(CommandSender sender) { - if (sender.hasPermission("adminmode.admin")) { - return true; - } + return sender.hasPermission("adminmode.admin"); + } - if (!(sender instanceof Player player)) { + public boolean isVanished(UUID playerId) { + return vanishedModerators.contains(playerId); + } + + public boolean toggleVanish(Player player) { + if (player == null || !hasSession(player.getUniqueId())) { return false; } + if (isVanished(player.getUniqueId())) { + disableVanish(player); + player.sendMessage(configManager.getPrefixedMessage("§7Vanish отключён.")); + return false; + } + enableVanish(player); + player.sendMessage(configManager.getPrefixedMessage("§aVanish включён.")); + return true; + } - ModeSession session = getSession(player); - return session != null && session.getProfile().isHardProfile(); + public void refreshVanishFor(Player viewer) { + if (viewer == null) { + return; + } + for (UUID vanishedId : vanishedModerators) { + Player vanishedPlayer = plugin.getServer().getPlayer(vanishedId); + if (vanishedPlayer != null && !viewer.getUniqueId().equals(vanishedId)) { + viewer.hidePlayer(plugin, vanishedPlayer); + } + } } public boolean teleportToPlayer(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; moderator.teleport(target.getLocation()); moderator.sendMessage(configManager.getPrefixedMessage("§aТелепортация к §f" + target.getName() + "§a выполнена.")); return true; } public boolean teleportPlayerHere(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; target.teleport(moderator.getLocation()); moderator.sendMessage(configManager.getPrefixedMessage("§aИгрок §f" + target.getName() + "§a телепортирован к вам.")); target.sendMessage(configManager.getPrefixedMessage("§eВас телепортировал модератор §f" + moderator.getName())); @@ -153,15 +173,11 @@ public class ModeManager { } public boolean spectatePlayer(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; if (moderator.equals(target)) { moderator.sendMessage(configManager.getPrefixedMessage("§cНельзя спектейтить самого себя.")); return false; } - moderator.setGameMode(GameMode.SPECTATOR); moderator.setSpectatorTarget((Entity) target); moderator.sendMessage(configManager.getPrefixedMessage("§bВы наблюдаете за §f" + target.getName())); @@ -169,20 +185,14 @@ public class ModeManager { } public boolean openInvsee(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - - moderator.openInventory(target.getInventory()); + if (moderator == null || target == null) return false; + moderator.getServer().dispatchCommand(moderator, "invsee " + target.getName()); moderator.sendMessage(configManager.getPrefixedMessage("§7Открыт инвентарь игрока §f" + target.getName())); return true; } public void applyAdminSettings(Player player, float flySpeed, float walkSpeed, boolean invulnerable) { - if (!hasSession(player.getUniqueId())) { - return; - } - + if (!hasSession(player.getUniqueId())) return; player.setAllowFlight(true); player.setFlying(true); player.setFlySpeed(clampSpeed(flySpeed, 0.1f)); @@ -192,7 +202,7 @@ public class ModeManager { public List listOtherOnlinePlayers(Player viewer) { List players = new ArrayList<>(plugin.getServer().getOnlinePlayers()); - players.removeIf(player -> viewer != null && player.getUniqueId().equals(viewer.getUniqueId())); + players.removeIf(p -> viewer != null && p.getUniqueId().equals(viewer.getUniqueId())); players.sort(Comparator.comparing(Player::getName, String.CASE_INSENSITIVE_ORDER)); return players; } @@ -200,10 +210,10 @@ public class ModeManager { public List buildOnlineSummary(Player viewer) { List lines = new ArrayList<>(); for (Player target : listOtherOnlinePlayers(viewer)) { - Location location = target.getLocation(); + Location loc = target.getLocation(); lines.add(Component.text( target.getName() + " | " + target.getWorld().getName() + " | " + - location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ(), + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ(), NamedTextColor.GRAY)); } if (lines.isEmpty()) { @@ -212,12 +222,11 @@ public class ModeManager { return lines; } - public void handleHardModeDeath(PlayerDeathEvent event) { + /** Keep inventory/levels and teleport back on death while in admin mode. */ + public void handleAdminDeath(PlayerDeathEvent event) { Player player = event.getEntity(); ModeSession session = getSession(player); - if (session == null || !session.getProfile().isHardProfile()) { - return; - } + if (session == null) return; event.setKeepInventory(true); event.setKeepLevel(true); @@ -228,45 +237,18 @@ public class ModeManager { } } - public boolean isPanelItem(ItemStack itemStack) { - return getPanelType(itemStack) != null; - } - - public String getPanelType(ItemStack itemStack) { - if (itemStack == null || itemStack.getType() == Material.AIR || !itemStack.hasItemMeta()) { - return null; - } - - ItemMeta itemMeta = itemStack.getItemMeta(); - PersistentDataContainer container = itemMeta.getPersistentDataContainer(); - if (!container.has(panelTypeKey, PersistentDataType.STRING)) { - return null; - } - return container.get(panelTypeKey, PersistentDataType.STRING); - } - public boolean isReadOnlyViewedInventory(Player viewer, org.bukkit.inventory.Inventory inventory) { - if (viewer == null || inventory == null || inventory.getHolder() == null) { - return false; - } - - if (!(inventory.getHolder() instanceof Player target)) { - return false; - } - + if (viewer == null || inventory == null || inventory.getHolder() == null) return false; + if (!(inventory.getHolder() instanceof Player target)) return false; return hasSession(viewer.getUniqueId()) && !viewer.getUniqueId().equals(target.getUniqueId()); } private boolean finishSession(Player player, boolean recovered) { - if (player == null) { - return false; - } - + if (player == null) return false; ModeSession session = activeSessions.remove(player.getUniqueId()); - if (session == null) { - return false; - } + if (session == null) return false; + disableVanish(player); restorePlayerState(player, session); luckPermsIntegration.exitMode(player, session); persistSessions(); @@ -281,14 +263,14 @@ public class ModeManager { return true; } - private void preparePlayerForModeration(Player player, ModeratorProfile profile) { + private void preparePlayerForModeration(Player player) { PlayerInventory inventory = player.getInventory(); inventory.clear(); inventory.setArmorContents(new ItemStack[4]); - inventory.setItemInOffHand(new ItemStack(Material.AIR)); + inventory.setItemInOffHand(ItemStack.empty()); player.closeInventory(); - player.setSpectatorTarget(null); + clearSpectatorTargetIfNeeded(player); for (PotionEffect effect : player.getActivePotionEffects()) { player.removePotionEffect(effect.getType()); } @@ -305,27 +287,35 @@ public class ModeManager { player.setFlying(true); player.setFlySpeed(clampSpeed(configManager.getCustomFlySpeed(), 0.1f)); player.setWalkSpeed(clampSpeed(configManager.getCustomWalkSpeed(), 0.1f)); - player.setInvulnerable(true); - player.setGameMode(profile.isHardProfile() ? GameMode.CREATIVE : GameMode.SPECTATOR); - - inventory.setItem(0, createPanelItem(profile.isHardProfile() ? configManager.getHardPanelMaterial() : configManager.getSoftPanelMaterial(), - profile.isHardProfile() ? "Hard Panel" : "Soft Panel", - profile.isHardProfile() ? NamedTextColor.RED : NamedTextColor.AQUA, - profile.isHardProfile() ? PANEL_HARD : PANEL_SOFT)); - - if (profile.isHardProfile()) { - inventory.setItem(1, createPanelItem( - configManager.getCpInspectorMaterial(), - "CP Inspector", - NamedTextColor.YELLOW, - PANEL_CP_INSPECTOR)); - } + player.setInvulnerable(configManager.isGodMode()); + player.setGameMode(resolveAdminGameMode(player)); if (configManager.isGivePresetBlocks()) { givePresetBlocks(player); } } + private void applyResumedModerationState(Player player) { + player.closeInventory(); + clearSpectatorTargetIfNeeded(player); + player.setFireTicks(0); + player.setInvulnerable(configManager.isGodMode()); + player.setFlySpeed(clampSpeed(player.getFlySpeed(), configManager.getCustomFlySpeed())); + player.setWalkSpeed(clampSpeed(player.getWalkSpeed(), configManager.getCustomWalkSpeed())); + + GameMode resumedMode = isAdminGameModeAllowedForPlayer(player, player.getGameMode()) + ? player.getGameMode() + : resolveAdminGameMode(player); + player.setGameMode(resumedMode); + + if (!player.getAllowFlight() && (resumedMode != GameMode.SURVIVAL || configManager.isAllowFlightInSurvival())) { + player.setAllowFlight(true); + } + if (player.getAllowFlight() && resumedMode != GameMode.SURVIVAL) { + player.setFlying(true); + } + } + private void restorePlayerState(Player player, ModeSession session) { PlayerInventory inventory = player.getInventory(); inventory.clear(); @@ -333,7 +323,7 @@ public class ModeManager { inventory.setArmorContents(session.getArmorContents()); inventory.setItemInOffHand(session.getOffHandItem()); - player.setSpectatorTarget(null); + clearSpectatorTargetIfNeeded(player); player.setInvulnerable(false); player.setAllowFlight(false); player.setFlying(false); @@ -346,7 +336,11 @@ public class ModeManager { player.setGameMode(session.getGameMode()); if (session.getLocation() != null) { - player.teleport(session.getLocation()); + player.teleportAsync(session.getLocation()).thenAccept(success -> { + if (!success) { + plugin.getLogger().warning("AdminMode: Не удалось телепортировать " + player.getName() + " обратно."); + } + }); } player.setHealth(Math.min(resolveMaxHealth(player), session.getHealth())); @@ -377,26 +371,96 @@ public class ModeManager { String[] parts = blockString.split(":"); Material material = Material.valueOf(parts[0].trim().toUpperCase()); int amount = parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 64; - player.getInventory().addItem(new ItemStack(material, Math.max(1, amount))); - } catch (Exception exception) { + player.getInventory().addItem(ItemStack.of(material, Math.max(1, amount))); + } catch (Exception e) { plugin.getLogger().warning("Неверный блок в preset_blocks: " + blockString); } } } - private ItemStack createPanelItem(Material material, String title, NamedTextColor color, String panelType) { - ItemStack itemStack = new ItemStack(material); - ItemMeta itemMeta = itemStack.getItemMeta(); - itemMeta.displayName(Component.text("[" + title + "]", color)); - itemMeta.getPersistentDataContainer().set(panelTypeKey, PersistentDataType.STRING, panelType); - itemStack.setItemMeta(itemMeta); - return itemStack; - } - private void persistSessions() { sessionStorage.saveSessions(activeSessions); } + private void clearSpectatorTargetIfNeeded(Player player) { + if (player.getGameMode() == GameMode.SPECTATOR) { + player.setSpectatorTarget(null); + } + } + + private GameMode resolveAdminGameMode(Player player) { + if (configManager.isForceSpectator()) { + return GameMode.SPECTATOR; + } + + ModeratorPreferencesStore.DefaultEntryMode preferredMode = preferencesStore.get(player.getUniqueId()).defaultEntryMode(); + return switch (preferredMode) { + case SURVIVAL -> GameMode.SURVIVAL; + case SPECTATOR -> GameMode.SPECTATOR; + case AUTO -> fallbackDefaultGameMode(player); + }; + } + + private GameMode fallbackDefaultGameMode(Player player) { + if (canUseCreativeInAdminMode(player)) { + return GameMode.CREATIVE; + } + return GameMode.SPECTATOR; + } + + private boolean canUseCreativeInAdminMode(Player player) { + return player.hasPermission("adminmode.chief") && configManager.isChiefCreativeMode(); + } + + private boolean isAdminGameModeAllowedForPlayer(Player player, GameMode gameMode) { + if (gameMode == null) { + return false; + } + if (configManager.isForceSpectator()) { + return gameMode == GameMode.SPECTATOR; + } + return switch (gameMode) { + case SURVIVAL, SPECTATOR -> true; + case CREATIVE -> canUseCreativeInAdminMode(player); + default -> false; + }; + } + + private void enableVanish(Player player) { + vanishedModerators.add(player.getUniqueId()); + player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0, false, false, false)); + player.setSilent(true); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.hidePlayer(plugin, player); + } + } + } + + private void disableVanish(Player player) { + if (!vanishedModerators.remove(player.getUniqueId())) { + return; + } + player.removePotionEffect(PotionEffectType.INVISIBILITY); + player.setSilent(false); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.showPlayer(plugin, player); + } + } + } + + private void reapplyVanish(Player player) { + vanishedModerators.add(player.getUniqueId()); + player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0, false, false, false)); + player.setSilent(true); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.hidePlayer(plugin, player); + } + } + } + private double resolveMaxHealth(Player player) { if (player.getAttribute(Attribute.MAX_HEALTH) != null) { return player.getAttribute(Attribute.MAX_HEALTH).getValue(); @@ -404,7 +468,8 @@ public class ModeManager { return 20.0D; } - private float clampSpeed(float value, float min) { - return Math.max(min, Math.min(1.0f, value)); + private float clampSpeed(float value, float fallback) { + float normalized = value <= 0.0f ? fallback : value; + return Math.max(0.1f, Math.min(1.0f, normalized)); } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeSession.java b/src/main/java/org/blz/adminmode/moderation/ModeSession.java old mode 100644 new mode 100755 index b2abab5..f84c49c --- a/src/main/java/org/blz/adminmode/moderation/ModeSession.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeSession.java @@ -246,7 +246,7 @@ public final class ModeSession { private static ItemStack cloneItem(ItemStack item) { if (item == null || item.getType() == Material.AIR) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } return item.clone(); } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java b/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java old mode 100644 new mode 100755 index 0d07509..3c63279 --- a/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java @@ -60,7 +60,7 @@ public class ModeSessionLogger { } private String modeSuffix(ModeSession session) { - return session.getProfile().isHardProfile() ? "HARD" : "SOFT"; + return "ADMIN"; } private String formatDuration(long millis) { diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java b/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java new file mode 100755 index 0000000..cb07877 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java @@ -0,0 +1,280 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.ArrayList; +import java.util.List; + +public class ModeratorAccessManager { + + private static final long PERMANENT_EXPIRY = -1L; + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final Map restrictions = new HashMap<>(); + + public ModeratorAccessManager(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "moderator-access.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public Optional getActiveRestriction(UUID playerId) { + RestrictionEntry entry = restrictions.get(playerId); + if (entry == null) { + return Optional.empty(); + } + if (entry.isExpired()) { + restrictions.remove(playerId); + save(); + return Optional.empty(); + } + return Optional.of(entry); + } + + public boolean isRestricted(UUID playerId) { + return getActiveRestriction(playerId).isPresent(); + } + + public RestrictionEntry restrict(OfflinePlayer target, String targetName, String actorName, Long expiresAt, String reason) { + RestrictionEntry entry = new RestrictionEntry( + target.getUniqueId(), + targetName, + actorName, + reason == null ? "" : reason.trim(), + System.currentTimeMillis(), + expiresAt == null ? PERMANENT_EXPIRY : expiresAt); + restrictions.put(target.getUniqueId(), entry); + save(); + return entry; + } + + public boolean clearRestriction(UUID playerId) { + RestrictionEntry removed = restrictions.remove(playerId); + if (removed != null) { + save(); + return true; + } + return false; + } + + public List getRestrictedPlayerNames() { + List names = new ArrayList<>(); + for (RestrictionEntry entry : restrictions.values()) { + if (!entry.isExpired() && entry.getPlayerName() != null && !entry.getPlayerName().isBlank()) { + names.add(entry.getPlayerName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + public String describeRestriction(UUID playerId) { + Optional optional = getActiveRestriction(playerId); + if (optional.isEmpty()) { + return ""; + } + RestrictionEntry entry = optional.get(); + StringBuilder message = new StringBuilder("§cВам запрещено входить в admin mode"); + if (!entry.isPermanent()) { + message.append(" §7(").append(formatRemaining(entry.getExpiresAt() - System.currentTimeMillis())).append("§7)"); + } else { + message.append(" §7(навсегда)"); + } + if (!entry.getReason().isBlank()) { + message.append("§c. Причина: §f").append(entry.getReason()); + } + return message.toString(); + } + + public static Long parseDurationToExpiry(String input) { + if (input == null || input.isBlank()) { + return null; + } + String normalized = input.trim().toLowerCase(); + if (normalized.equals("perm") || normalized.equals("permanent") || normalized.equals("forever") + || normalized.equals("навсегда")) { + return null; + } + if (!normalized.matches("\\d+[smhdwy]")) { + return Long.MIN_VALUE; + } + + long amount = Long.parseLong(normalized.substring(0, normalized.length() - 1)); + char suffix = normalized.charAt(normalized.length() - 1); + Duration duration = switch (suffix) { + case 's' -> Duration.ofSeconds(amount); + case 'm' -> Duration.ofMinutes(amount); + case 'h' -> Duration.ofHours(amount); + case 'd' -> Duration.ofDays(amount); + case 'w' -> Duration.ofDays(amount * 7); + case 'y' -> Duration.ofDays(amount * 365); + default -> null; + }; + if (duration == null || duration.isNegative() || duration.isZero()) { + return Long.MIN_VALUE; + } + return System.currentTimeMillis() + duration.toMillis(); + } + + private void load() { + restrictions.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonArray()) { + return; + } + + for (JsonElement element : root.getAsJsonArray()) { + if (!element.isJsonObject()) { + continue; + } + RestrictionEntry entry = deserialize(element.getAsJsonObject()); + if (entry != null && !entry.isExpired()) { + restrictions.put(entry.getPlayerId(), entry); + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить moderator-access.json: " + exception.getMessage()); + } + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonArray array = new JsonArray(); + for (RestrictionEntry entry : restrictions.values()) { + if (!entry.isExpired()) { + array.add(serialize(entry)); + } + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(array)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить moderator-access.json: " + exception.getMessage()); + } + } + + private JsonObject serialize(RestrictionEntry entry) { + JsonObject json = new JsonObject(); + json.addProperty("playerId", entry.getPlayerId().toString()); + json.addProperty("playerName", entry.getPlayerName()); + json.addProperty("actorName", entry.getActorName()); + json.addProperty("reason", entry.getReason()); + json.addProperty("createdAt", entry.getCreatedAt()); + json.addProperty("expiresAt", entry.getExpiresAt()); + return json; + } + + private RestrictionEntry deserialize(JsonObject json) { + try { + UUID playerId = UUID.fromString(json.get("playerId").getAsString()); + String playerName = json.has("playerName") ? json.get("playerName").getAsString() : playerId.toString(); + String actorName = json.has("actorName") ? json.get("actorName").getAsString() : "unknown"; + String reason = json.has("reason") ? json.get("reason").getAsString() : ""; + long createdAt = json.has("createdAt") ? json.get("createdAt").getAsLong() : System.currentTimeMillis(); + long expiresAt = json.has("expiresAt") ? json.get("expiresAt").getAsLong() : PERMANENT_EXPIRY; + return new RestrictionEntry(playerId, playerName, actorName, reason, createdAt, expiresAt); + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось прочитать запись ограничения admin mode: " + exception.getMessage()); + return null; + } + } + + private String formatRemaining(long millis) { + Duration duration = Duration.ofMillis(Math.max(millis, 0L)); + long days = duration.toDays(); + long hours = duration.toHoursPart(); + long minutes = duration.toMinutesPart(); + + if (days > 0) { + return days + "д " + hours + "ч"; + } + if (duration.toHours() > 0) { + return duration.toHours() + "ч " + minutes + "м"; + } + if (duration.toMinutes() > 0) { + return duration.toMinutes() + "м"; + } + if (duration.toSeconds() > 0) { + return duration.toSeconds() + "с"; + } + return Math.max(1L, duration.toMinutes()) + "м"; + } + + public static final class RestrictionEntry { + private final UUID playerId; + private final String playerName; + private final String actorName; + private final String reason; + private final long createdAt; + private final long expiresAt; + + public RestrictionEntry(UUID playerId, String playerName, String actorName, String reason, long createdAt, long expiresAt) { + this.playerId = playerId; + this.playerName = playerName; + this.actorName = actorName; + this.reason = reason; + this.createdAt = createdAt; + this.expiresAt = expiresAt; + } + + public UUID getPlayerId() { + return playerId; + } + + public String getPlayerName() { + return playerName; + } + + public String getActorName() { + return actorName; + } + + public String getReason() { + return reason; + } + + public long getCreatedAt() { + return createdAt; + } + + public long getExpiresAt() { + return expiresAt; + } + + public boolean isPermanent() { + return expiresAt == PERMANENT_EXPIRY; + } + + public boolean isExpired() { + return !isPermanent() && System.currentTimeMillis() >= expiresAt; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java b/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java new file mode 100755 index 0000000..d3199b6 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java @@ -0,0 +1,230 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +public class ModeratorPreferencesStore { + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final Map preferences = new LinkedHashMap<>(); + + public ModeratorPreferencesStore(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "moderator-preferences.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public ModeratorPreferences get(UUID playerId) { + return preferences.computeIfAbsent(playerId, ignored -> new ModeratorPreferences()); + } + + public DefaultEntryMode cycleDefaultEntryMode(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + DefaultEntryMode[] values = new DefaultEntryMode[]{DefaultEntryMode.AUTO, DefaultEntryMode.SURVIVAL, DefaultEntryMode.SPECTATOR}; + preferences.defaultEntryMode = cycle(values, preferences.defaultEntryMode); + save(); + return preferences.defaultEntryMode; + } + + public MenuTheme cycleTheme(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + preferences.theme = cycle(MenuTheme.values(), preferences.theme); + save(); + return preferences.theme; + } + + public AccentColor cycleAccentColor(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + preferences.accentColor = cycle(AccentColor.values(), preferences.accentColor); + save(); + return preferences.accentColor; + } + + private T cycle(T[] values, T current) { + for (int i = 0; i < values.length; i++) { + if (values[i] == current) { + return values[(i + 1) % values.length]; + } + } + return values[0]; + } + + private void load() { + preferences.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonObject()) { + return; + } + + for (Map.Entry entry : root.getAsJsonObject().entrySet()) { + if (!entry.getValue().isJsonObject()) { + continue; + } + try { + UUID uuid = UUID.fromString(entry.getKey()); + preferences.put(uuid, readPreferences(entry.getValue().getAsJsonObject())); + } catch (IllegalArgumentException ignored) { + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить moderator-preferences.json: " + exception.getMessage()); + } + } + + private ModeratorPreferences readPreferences(JsonObject json) { + ModeratorPreferences result = new ModeratorPreferences(); + result.defaultEntryMode = DefaultEntryMode.fromName(getString(json, "defaultEntryMode")); + result.theme = MenuTheme.fromName(getString(json, "theme")); + result.accentColor = AccentColor.fromName(getString(json, "accentColor")); + return result; + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonObject root = new JsonObject(); + for (Map.Entry entry : preferences.entrySet()) { + JsonObject json = new JsonObject(); + json.addProperty("defaultEntryMode", entry.getValue().defaultEntryMode.name()); + json.addProperty("theme", entry.getValue().theme.name()); + json.addProperty("accentColor", entry.getValue().accentColor.name()); + root.add(entry.getKey().toString(), json); + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(root)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить moderator-preferences.json: " + exception.getMessage()); + } + } + + private String getString(JsonObject json, String key) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : ""; + } + + public static final class ModeratorPreferences { + private DefaultEntryMode defaultEntryMode = DefaultEntryMode.AUTO; + private MenuTheme theme = MenuTheme.RAINBOW; + private AccentColor accentColor = AccentColor.AQUA; + + public DefaultEntryMode defaultEntryMode() { + return defaultEntryMode; + } + + public MenuTheme theme() { + return theme; + } + + public AccentColor accentColor() { + return accentColor; + } + } + + public enum DefaultEntryMode { + AUTO("По серверу"), + SURVIVAL("SURVIVAL"), + SPECTATOR("SPECTATOR"); + + private final String displayName; + + DefaultEntryMode(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + + public static DefaultEntryMode fromName(String value) { + for (DefaultEntryMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + return AUTO; + } + } + + public enum MenuTheme { + RAINBOW("Радужная"), + CLEAN_ACCENT("Светлая акцентная"), + WHITE("Белая"); + + private final String displayName; + + MenuTheme(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + + public static MenuTheme fromName(String value) { + for (MenuTheme theme : values()) { + if (theme.name().equalsIgnoreCase(value)) { + return theme; + } + } + return RAINBOW; + } + } + + public enum AccentColor { + AQUA("Аква", NamedTextColor.AQUA), + GOLD("Золото", NamedTextColor.GOLD), + GREEN("Лайм", NamedTextColor.GREEN), + YELLOW("Жёлтый", NamedTextColor.YELLOW), + RED("Красный", NamedTextColor.RED), + BLUE("Синий", NamedTextColor.BLUE); + + private final String displayName; + private final NamedTextColor textColor; + + AccentColor(String displayName, NamedTextColor textColor) { + this.displayName = displayName; + this.textColor = textColor; + } + + public String displayName() { + return displayName; + } + + public NamedTextColor textColor() { + return textColor; + } + + public static AccentColor fromName(String value) { + for (AccentColor color : values()) { + if (color.name().equalsIgnoreCase(value)) { + return color; + } + } + return AQUA; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java b/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java old mode 100644 new mode 100755 index 5044501..bed2f4e --- a/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java @@ -2,14 +2,10 @@ package org.blz.adminmode.moderation; import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; import java.util.Optional; public enum ModeratorProfile { - SOFT_MODER("soft_moder", "Soft Moder", "adminmode.profile.soft"), - HARD_MODER("hard_moder", "Hard Moder", "adminmode.profile.hard"); + ADMIN("admin", "Admin Mode", "adminmode.use"); private final String configKey; private final String displayName; @@ -33,54 +29,12 @@ public enum ModeratorProfile { return permission; } - public boolean isHardProfile() { - return this == HARD_MODER; - } - public boolean isAvailableFor(Player player) { - return switch (this) { - case SOFT_MODER -> player.hasPermission("adminmode.use") - || player.hasPermission(permission) - || player.hasPermission(HARD_MODER.permission); - case HARD_MODER -> player.hasPermission(permission); - }; - } - - public static List availableFor(Player player) { - List profiles = new ArrayList<>(); - for (ModeratorProfile profile : values()) { - if (profile.isAvailableFor(player)) { - profiles.add(profile); - } - } - return profiles; + return player.hasPermission(permission); } + /** Accepts any old key (soft_moder, hard_moder, admin) and returns ADMIN. */ public static Optional fromConfigKey(String key) { - if (key == null || key.isBlank()) { - return Optional.empty(); - } - - String normalized = key.toLowerCase(Locale.ROOT); - for (ModeratorProfile profile : values()) { - if (profile.configKey.equals(normalized)) { - return Optional.of(profile); - } - } - - return Optional.empty(); - } - - public static Optional fromInput(String input) { - if (input == null || input.isBlank()) { - return Optional.empty(); - } - - String normalized = input.trim().toLowerCase(Locale.ROOT); - return switch (normalized) { - case "soft", "soft_moder", "soft-moder", "softmoder" -> Optional.of(SOFT_MODER); - case "hard", "hard_moder", "hard-moder", "hardmoder" -> Optional.of(HARD_MODER); - default -> Optional.empty(); - }; + return Optional.of(ADMIN); } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java b/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java new file mode 100644 index 0000000..9eb541b --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java @@ -0,0 +1,256 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class ModeratorWarningStore { + + private static final long NOT_REVOKED = -1L; + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final List entries = new ArrayList<>(); + + public ModeratorWarningStore(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "warn-history.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public WarningEntry recordWarn(Player target, Player moderator, String reason, String durationToken) { + if (target == null || moderator == null) { + return null; + } + + long now = System.currentTimeMillis(); + long expiresAt = PunishmentDuration.toExpiry(durationToken, now); + WarningEntry entry = new WarningEntry( + UUID.randomUUID(), + target.getUniqueId(), + target.getName(), + moderator.getName(), + sanitizeReason(reason), + now, + expiresAt, + NOT_REVOKED, + "", + ""); + entries.add(entry); + save(); + return entry; + } + + public List getRecentWarnings(UUID playerId, int limit) { + return entries.stream() + .filter(entry -> entry.playerId.equals(playerId)) + .sorted(Comparator.comparingLong(WarningEntry::createdAt).reversed()) + .limit(Math.max(0, limit)) + .toList(); + } + + public List getActiveWarnings(UUID playerId) { + return entries.stream() + .filter(entry -> entry.playerId.equals(playerId) && entry.status() == WarningStatus.ACTIVE) + .sorted(Comparator.comparingLong(WarningEntry::createdAt).reversed()) + .toList(); + } + + public Optional findWarning(UUID warningId) { + if (warningId == null) { + return Optional.empty(); + } + return entries.stream().filter(entry -> entry.id.equals(warningId)).findFirst(); + } + + public boolean revokeWarning(UUID warningId, Player moderator, String reason) { + if (warningId == null || moderator == null || reason == null || reason.isBlank()) { + return false; + } + + for (int index = 0; index < entries.size(); index++) { + WarningEntry current = entries.get(index); + if (!current.id.equals(warningId) || current.status() != WarningStatus.ACTIVE) { + continue; + } + entries.set(index, current.revokedBy(moderator.getName(), sanitizeReason(reason), System.currentTimeMillis())); + save(); + return true; + } + return false; + } + + private String sanitizeReason(String reason) { + if (reason == null || reason.isBlank()) { + return "без причины"; + } + return reason.replace('\n', ' ').replace('\r', ' ').trim(); + } + + private void load() { + entries.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonArray()) { + return; + } + for (JsonElement element : root.getAsJsonArray()) { + if (!element.isJsonObject()) { + continue; + } + WarningEntry entry = readEntry(element.getAsJsonObject()); + if (entry != null) { + entries.add(entry); + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить warn-history.json: " + exception.getMessage()); + } + } + + private WarningEntry readEntry(JsonObject json) { + try { + long createdAt = json.get("createdAt").getAsLong(); + boolean legacyActive = !json.has("active") || json.get("active").getAsBoolean(); + long revokedAt = json.has("revokedAt") + ? json.get("revokedAt").getAsLong() + : legacyActive ? NOT_REVOKED : 0L; + return new WarningEntry( + json.has("id") ? UUID.fromString(json.get("id").getAsString()) : UUID.randomUUID(), + UUID.fromString(json.get("playerId").getAsString()), + json.get("playerName").getAsString(), + json.get("moderatorName").getAsString(), + json.get("reason").getAsString(), + createdAt, + json.has("expiresAt") ? json.get("expiresAt").getAsLong() : PunishmentDuration.PERMANENT_EXPIRY, + revokedAt, + readString(json, "revokedBy", legacyActive ? "" : "неизвестно"), + readString(json, "revocationReason", legacyActive ? "" : "данные отсутствуют")); + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить запись warn: " + exception.getMessage()); + return null; + } + } + + private String readString(JsonObject json, String key, String fallback) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : fallback; + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonArray root = new JsonArray(); + for (WarningEntry entry : entries) { + JsonObject json = new JsonObject(); + json.addProperty("id", entry.id.toString()); + json.addProperty("playerId", entry.playerId.toString()); + json.addProperty("playerName", entry.playerName); + json.addProperty("moderatorName", entry.moderatorName); + json.addProperty("reason", entry.reason); + json.addProperty("createdAt", entry.createdAt); + json.addProperty("expiresAt", entry.expiresAt); + json.addProperty("revokedAt", entry.revokedAt); + json.addProperty("revokedBy", entry.revokedBy); + json.addProperty("revocationReason", entry.revocationReason); + json.addProperty("active", entry.status() == WarningStatus.ACTIVE); + root.add(json); + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(root)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить warn-history.json: " + exception.getMessage()); + } + } + + public enum WarningStatus { + ACTIVE, + EXPIRED, + REVOKED + } + + public static final class WarningEntry { + private final UUID id; + private final UUID playerId; + private final String playerName; + private final String moderatorName; + private final String reason; + private final long createdAt; + private final long expiresAt; + private final long revokedAt; + private final String revokedBy; + private final String revocationReason; + + private WarningEntry(UUID id, UUID playerId, String playerName, String moderatorName, String reason, + long createdAt, long expiresAt, long revokedAt, String revokedBy, String revocationReason) { + this.id = id; + this.playerId = playerId; + this.playerName = playerName; + this.moderatorName = moderatorName; + this.reason = reason; + this.createdAt = createdAt; + this.expiresAt = expiresAt; + this.revokedAt = revokedAt; + this.revokedBy = revokedBy; + this.revocationReason = revocationReason; + } + + private WarningEntry revokedBy(String actor, String revocationReason, long timestamp) { + return new WarningEntry(id, playerId, playerName, moderatorName, reason, createdAt, expiresAt, + timestamp, actor, revocationReason); + } + + public UUID id() { return id; } + public UUID playerId() { return playerId; } + public String playerName() { return playerName; } + public String moderatorName() { return moderatorName; } + public String reason() { return reason; } + public long createdAt() { return createdAt; } + public long expiresAt() { return expiresAt; } + public long revokedAt() { return revokedAt; } + public String revokedBy() { return revokedBy; } + public String revocationReason() { return revocationReason; } + + public boolean isPermanent() { + return expiresAt == PunishmentDuration.PERMANENT_EXPIRY; + } + + public WarningStatus status() { + if (revokedAt != NOT_REVOKED) { + return WarningStatus.REVOKED; + } + if (!isPermanent() && System.currentTimeMillis() >= expiresAt) { + return WarningStatus.EXPIRED; + } + return WarningStatus.ACTIVE; + } + + public boolean active() { + return status() == WarningStatus.ACTIVE; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java b/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java new file mode 100755 index 0000000..0a3c626 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java @@ -0,0 +1,127 @@ +package org.blz.adminmode.moderation; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class PlayerFreezeManager implements Listener { + + private static final long NOTIFY_COOLDOWN_MS = 1500L; + + private final Plugin plugin; + private final Set frozenPlayers = new HashSet<>(); + private final Map lastNotify = new HashMap<>(); + + public PlayerFreezeManager(Plugin plugin) { + this.plugin = plugin; + } + + public boolean toggleFreeze(Player target) { + UUID uuid = target.getUniqueId(); + if (frozenPlayers.remove(uuid)) { + target.sendMessage(Component.text("Вы больше не заморожены.", NamedTextColor.GREEN)); + return false; + } + frozenPlayers.add(uuid); + target.sendMessage(Component.text("Вы заморожены. Любые действия заблокированы.", NamedTextColor.RED)); + return true; + } + + public boolean isFrozen(UUID playerId) { + return frozenPlayers.contains(playerId); + } + + private boolean shouldBlock(Player player) { + return frozenPlayers.contains(player.getUniqueId()); + } + + private void notifyFrozen(Player player) { + long now = System.currentTimeMillis(); + long previous = lastNotify.getOrDefault(player.getUniqueId(), 0L); + if (now - previous < NOTIFY_COOLDOWN_MS) { + return; + } + lastNotify.put(player.getUniqueId(), now); + player.sendActionBar(Component.text("Вы заморожены модератором.", NamedTextColor.RED)); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + if (!shouldBlock(player) || event.getTo() == null) return; + if (event.getFrom().distanceSquared(event.getTo()) > 0.0D) { + event.setTo(event.getFrom()); + notifyFrozen(player); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInteract(PlayerInteractEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onDrop(PlayerDropItemEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onPickup(EntityPickupItemEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onCommand(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java b/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java old mode 100644 new mode 100755 index dc9005f..90eb3c5 --- a/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java +++ b/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java @@ -3,5 +3,6 @@ package org.blz.adminmode.moderation; public enum PlayerListAction { SPECTATE, TELEPORT, - INVSEE + INVSEE, + MANAGE } diff --git a/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java b/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java new file mode 100644 index 0000000..9457f3a --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java @@ -0,0 +1,57 @@ +package org.blz.adminmode.moderation; + +import java.time.Duration; +import java.util.Locale; + +public final class PunishmentDuration { + + public static final long PERMANENT_EXPIRY = -1L; + + private PunishmentDuration() { + } + + public static String normalizeToken(String input) { + if (input == null || input.isBlank()) { + return ""; + } + + String normalized = input.trim().toLowerCase(Locale.ROOT); + if (normalized.equals("perm") || normalized.equals("permanent") || normalized.equals("permanently")) { + return "permanent"; + } + if (!normalized.matches("[1-9]\\d*[smhdwy]")) { + return ""; + } + + try { + toDuration(normalized); + return normalized; + } catch (ArithmeticException | IllegalArgumentException exception) { + return ""; + } + } + + public static long toExpiry(String token, long now) { + String normalized = normalizeToken(token); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Unsupported punishment duration: " + token); + } + if (normalized.equals("permanent")) { + return PERMANENT_EXPIRY; + } + return Math.addExact(now, toDuration(normalized).toMillis()); + } + + private static Duration toDuration(String normalized) { + long amount = Long.parseLong(normalized.substring(0, normalized.length() - 1)); + return switch (normalized.charAt(normalized.length() - 1)) { + case 's' -> Duration.ofSeconds(amount); + case 'm' -> Duration.ofMinutes(amount); + case 'h' -> Duration.ofHours(amount); + case 'd' -> Duration.ofDays(amount); + case 'w' -> Duration.ofDays(Math.multiplyExact(amount, 7L)); + case 'y' -> Duration.ofDays(Math.multiplyExact(amount, 365L)); + default -> throw new IllegalArgumentException("Unsupported punishment duration: " + normalized); + }; + } +} diff --git a/src/main/java/org/blz/adminmode/session/SessionStorage.java b/src/main/java/org/blz/adminmode/session/SessionStorage.java old mode 100644 new mode 100755 index 1089afd..e1ffab3 --- a/src/main/java/org/blz/adminmode/session/SessionStorage.java +++ b/src/main/java/org/blz/adminmode/session/SessionStorage.java @@ -8,29 +8,22 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.blz.adminmode.moderation.ModeSession; import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.utils.ItemStackCodec; +import org.blz.adminmode.utils.PotionEffectCodec; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -128,7 +121,7 @@ public class SessionStorage { UUID playerId = UUID.fromString(json.get("playerId").getAsString()); String playerName = json.get("playerName").getAsString(); ModeratorProfile profile = ModeratorProfile.fromConfigKey(json.get("profile").getAsString()) - .orElse(ModeratorProfile.SOFT_MODER); + .orElse(ModeratorProfile.ADMIN); long startTimestamp = json.get("startTimestamp").getAsLong(); String previousGroup = getString(json, "previousGroup"); String activeGroup = getString(json, "activeGroup"); @@ -224,14 +217,7 @@ public class SessionStorage { private String serializeItemArray(ItemStack[] items) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeInt(items.length); - for (ItemStack item : items) { - dataOutput.writeObject(item); - } - } - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItems(items); } catch (Exception exception) { plugin.getLogger().warning("Не удалось сериализовать item array: " + exception.getMessage()); return ""; @@ -244,15 +230,7 @@ public class SessionStorage { } try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { - int length = dataInput.readInt(); - ItemStack[] items = new ItemStack[length]; - for (int i = 0; i < length; i++) { - items[i] = (ItemStack) dataInput.readObject(); - } - return items; - } + return ItemStackCodec.deserializeItems(data); } catch (Exception exception) { plugin.getLogger().warning("Не удалось десериализовать item array: " + exception.getMessage()); return new ItemStack[0]; @@ -261,11 +239,7 @@ public class SessionStorage { private String serializeItem(ItemStack item) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeObject(item); - } - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItem(item); } catch (Exception exception) { plugin.getLogger().warning("Не удалось сериализовать item: " + exception.getMessage()); return ""; @@ -274,62 +248,24 @@ public class SessionStorage { private ItemStack deserializeItem(String data) { if (data == null || data.isEmpty()) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { - return (ItemStack) dataInput.readObject(); - } + return ItemStackCodec.deserializeItem(data); } catch (Exception exception) { plugin.getLogger().warning("Не удалось десериализовать item: " + exception.getMessage()); - return new ItemStack(Material.AIR); + return ItemStack.empty(); } } private String serializePotionEffects(Collection effects) { - StringBuilder builder = new StringBuilder(); - for (PotionEffect effect : effects) { - builder.append(effect.getType().getName()).append(':') - .append(effect.getDuration()).append(':') - .append(effect.getAmplifier()).append(':') - .append(effect.isAmbient()).append(':') - .append(effect.hasParticles()).append(':') - .append(effect.hasIcon()).append(';'); - } - return builder.toString(); + return PotionEffectCodec.serialize(effects); } private Collection deserializePotionEffects(String data) { - List effects = new ArrayList<>(); - if (data == null || data.isEmpty()) { - return effects; - } - - String[] effectStrings = data.split(";"); - for (String effectString : effectStrings) { - if (effectString.isEmpty()) { - continue; - } - - try { - String[] parts = effectString.split(":"); - PotionEffectType type = PotionEffectType.getByName(parts[0]); - if (type == null) { - continue; - } - effects.add(new PotionEffect( - type, - Integer.parseInt(parts[1]), - Integer.parseInt(parts[2]), - Boolean.parseBoolean(parts[3]), - Boolean.parseBoolean(parts[4]), - Boolean.parseBoolean(parts[5]))); - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось десериализовать potion effect: " + effectString); - } - } - return effects; + return PotionEffectCodec.deserialize(data, + encodedEffect -> plugin.getLogger().warning( + "Не удалось десериализовать potion effect: " + encodedEffect)); } } diff --git a/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java b/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java new file mode 100644 index 0000000..dac4328 --- /dev/null +++ b/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java @@ -0,0 +1,85 @@ +package org.blz.adminmode.utils; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; + +/** + * Stores item stacks using Paper's data-version-aware NBT format while retaining + * read compatibility with sessions written by older AdminMode releases. + */ +public final class ItemStackCodec { + + private static final String ITEM_FORMAT_PREFIX = "nbt-item-v1:"; + private static final String ITEMS_FORMAT_PREFIX = "nbt-items-v1:"; + + private ItemStackCodec() { + } + + public static String serializeItems(ItemStack[] items) { + ItemStack[] safeItems = items == null ? new ItemStack[0] : items; + return ITEMS_FORMAT_PREFIX + + Base64.getEncoder().encodeToString(ItemStack.serializeItemsAsBytes(safeItems)); + } + + public static ItemStack[] deserializeItems(String data) throws IOException, ClassNotFoundException { + if (data == null || data.isEmpty()) { + return new ItemStack[0]; + } + if (data.startsWith(ITEMS_FORMAT_PREFIX)) { + byte[] bytes = decode(data.substring(ITEMS_FORMAT_PREFIX.length())); + return ItemStack.deserializeItemsFromBytes(bytes); + } + return deserializeLegacyItems(data); + } + + public static String serializeItem(ItemStack item) { + ItemStack safeItem = item == null ? ItemStack.empty() : item; + return ITEM_FORMAT_PREFIX + + Base64.getEncoder().encodeToString(safeItem.serializeAsBytes()); + } + + public static ItemStack deserializeItem(String data) throws IOException, ClassNotFoundException { + if (data == null || data.isEmpty()) { + return ItemStack.empty(); + } + if (data.startsWith(ITEM_FORMAT_PREFIX)) { + byte[] bytes = decode(data.substring(ITEM_FORMAT_PREFIX.length())); + return ItemStack.deserializeBytes(bytes); + } + return deserializeLegacyItem(data); + } + + private static byte[] decode(String data) throws IOException { + try { + return Base64.getDecoder().decode(data); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid Base64 item data", exception); + } + } + + @SuppressWarnings("deprecation") + private static ItemStack[] deserializeLegacyItems(String data) throws IOException, ClassNotFoundException { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { + int length = dataInput.readInt(); + ItemStack[] items = new ItemStack[length]; + for (int i = 0; i < length; i++) { + items[i] = (ItemStack) dataInput.readObject(); + } + return items; + } + } + + @SuppressWarnings("deprecation") + private static ItemStack deserializeLegacyItem(String data) throws IOException, ClassNotFoundException { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { + ItemStack item = (ItemStack) dataInput.readObject(); + return item == null ? ItemStack.empty() : item; + } + } +} diff --git a/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java b/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java new file mode 100644 index 0000000..97b5fd5 --- /dev/null +++ b/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java @@ -0,0 +1,86 @@ +package org.blz.adminmode.utils; + +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +/** Encodes potion effects with registry keys and reads the legacy enum-name format. */ +public final class PotionEffectCodec { + + private static final String FORMAT_PREFIX = "potion-v2:"; + + private PotionEffectCodec() { + } + + public static String serialize(Collection effects) { + StringBuilder builder = new StringBuilder(FORMAT_PREFIX); + if (effects == null) { + return builder.toString(); + } + + for (PotionEffect effect : effects) { + NamespacedKey key = Registry.MOB_EFFECT.getKey(effect.getType()); + builder.append(key).append('|') + .append(effect.getDuration()).append('|') + .append(effect.getAmplifier()).append('|') + .append(effect.isAmbient()).append('|') + .append(effect.hasParticles()).append('|') + .append(effect.hasIcon()).append(';'); + } + return builder.toString(); + } + + public static Collection deserialize(String data, Consumer warningSink) { + List effects = new ArrayList<>(); + if (data == null || data.isEmpty()) { + return effects; + } + + boolean currentFormat = data.startsWith(FORMAT_PREFIX); + String payload = currentFormat ? data.substring(FORMAT_PREFIX.length()) : data; + for (String encodedEffect : payload.split(";")) { + if (encodedEffect.isEmpty()) { + continue; + } + + try { + String[] parts = currentFormat + ? encodedEffect.split("\\|", -1) + : encodedEffect.split(":", -1); + if (parts.length != 6) { + throw new IllegalArgumentException("Expected 6 fields"); + } + + PotionEffectType type = resolveType(parts[0], currentFormat); + if (type == null) { + throw new IllegalArgumentException("Unknown effect " + parts[0]); + } + + effects.add(new PotionEffect( + type, + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2]), + Boolean.parseBoolean(parts[3]), + Boolean.parseBoolean(parts[4]), + Boolean.parseBoolean(parts[5]))); + } catch (RuntimeException exception) { + warningSink.accept(encodedEffect); + } + } + return effects; + } + + private static PotionEffectType resolveType(String identifier, boolean currentFormat) { + NamespacedKey key = currentFormat + ? NamespacedKey.fromString(identifier) + : NamespacedKey.minecraft(identifier.toLowerCase(Locale.ROOT)); + return key == null ? null : Registry.MOB_EFFECT.get(key); + } +} diff --git a/src/main/java/org/blz/adminmode/utils/ScreenEffects.java b/src/main/java/org/blz/adminmode/utils/ScreenEffects.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/blz/adminmode/utils/WorldLoader.java b/src/main/java/org/blz/adminmode/utils/WorldLoader.java old mode 100644 new mode 100755 index 0098173..e42d9f8 --- a/src/main/java/org/blz/adminmode/utils/WorldLoader.java +++ b/src/main/java/org/blz/adminmode/utils/WorldLoader.java @@ -38,6 +38,7 @@ public class WorldLoader { return; } } + cleanupSkippedFiles(worldFolder); plugin.getLogger().info("Загрузка мира " + worldName + "..."); WorldCreator creator = new WorldCreator(worldName); @@ -64,8 +65,18 @@ public class WorldLoader { try (ZipInputStream zipIn = new ZipInputStream(in)) { ZipEntry entry = zipIn.getNextEntry(); + String destinationPath = destination.getCanonicalPath() + File.separator; while (entry != null) { + if (shouldSkipEntry(entry.getName())) { + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + continue; + } File file = new File(destination, entry.getName()); + String filePath = file.getCanonicalPath(); + if (!filePath.startsWith(destinationPath)) { + throw new IOException("Blocked unsafe zip entry: " + entry.getName()); + } if (entry.isDirectory()) { file.mkdirs(); } else { @@ -91,4 +102,52 @@ public class WorldLoader { return false; } } + + private boolean shouldSkipEntry(String entryName) { + String normalized = entryName.replace('\\', '/'); + return normalized.equals("level.dat") + || normalized.equals("level.dat_old") + || normalized.equals("session.lock") + || normalized.equals("icon.png") + || normalized.equals("xaeromap.txt") + || normalized.startsWith("serverconfig/") + || normalized.startsWith("playerdata/") + || normalized.startsWith("stats/") + || normalized.startsWith("advancements/"); + } + + private void cleanupSkippedFiles(File destination) { + deleteIfExists(new File(destination, "level.dat")); + deleteIfExists(new File(destination, "level.dat_old")); + deleteIfExists(new File(destination, "session.lock")); + deleteIfExists(new File(destination, "icon.png")); + deleteIfExists(new File(destination, "xaeromap.txt")); + deleteRecursively(new File(destination, "serverconfig")); + deleteRecursively(new File(destination, "playerdata")); + deleteRecursively(new File(destination, "stats")); + deleteRecursively(new File(destination, "advancements")); + } + + private void deleteIfExists(File file) { + if (file.exists() && !file.delete()) { + plugin.getLogger().warning("Не удалось удалить файл " + file.getAbsolutePath()); + } + } + + private void deleteRecursively(File file) { + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + if (!file.delete()) { + plugin.getLogger().warning("Не удалось удалить " + file.getAbsolutePath()); + } + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml old mode 100644 new mode 100755 index 0dbd2bf..ff81769 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,77 +1,43 @@ # AdminMode Configuration -#РЕЖИМ АДМИНИСТРАТОРА admin_mode: - # Включен ли режим администратора enabled: true - - # LuckPerms настройки + luckperms: - # Использовать ли LuckPerms (если false - используется стандартный OP) enabled: true - - # Группы которые выдаются при активации режима - # Можно указать несколько групп через запятую + # LuckPerms-группы, которые выдаются при входе в режим groups: - - "soft_moder_adm" - profile_groups: - soft_moder: - - "soft_moder_adm" - hard_moder: - - "hard_moder_adm" - - # Удалять ли группы при деактивации (если false - группы останутся) + - "admin_moder" remove_on_disable: true dialogs: + # Глобальный мастер-переключатель Dialog API (false = отключить для всего сервера). + # Каждый модератор лично включает/выключает своё GUI командой /adminmode dialog enabled: true - - # Настройки игрового режима + gamemode: - # Принудительно ставить спектатор (если false - survival) + # false = chiefs can use CREATIVE, regular moderators use SPECTATOR + # true = everyone enters in SPECTATOR force_spectator: false - - # Мобы игнорируют игрока в админ моде mobs_ignore_player: true - - # Разрешить полет в режиме выживания (в админ моде) allow_flight_in_survival: true - - # Режим бога (неуязвимость) god_mode: true - - # Кастомная скорость полета (0.1 = медленно, 0.2 = нормально, 0.5 = быстро, 1.0 = очень быстро) + chief_creative: true + # Скорость полёта: 0.1 (медленно) ... 1.0 (максимум) custom_fly_speed: 0.2 - - # Кастомная скорость ходьбы (0.1 = медленно, 0.2 = нормально, 0.5 = быстро) custom_walk_speed: 0.2 - - # Разрешенные режимы игры для переключения в админ моде - # Если список пустой - можно переключаться на любые режимы - # Доступные: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR - # ВАЖНО: CREATIVE убран - модеры не должны иметь доступ к креативу! + # Разрешённые режимы для переключения внутри админ-мода (пусто = любые) allowed_gamemodes: + - "CREATIVE" - "SURVIVAL" - "SPECTATOR" - - # Ограничения в режиме администратора + restrictions: - # Запретить выбрасывание предметов prevent_item_drop: false - - # Запретить поднятие предметов prevent_item_pickup: false - - # Запретить перемещение предметов в контейнеры (сундуки, бочки и т.д.) prevent_container_transfer: true - - # Запретить использование предметов (кроме открытия контейнеров) prevent_item_use: false - - # Разрешить управление своим инвентарем allow_own_inventory: true - - # Разрешенные блоки для взаимодействия (открытие, использование) allowed_blocks: - "CHEST" - "TRAPPED_CHEST" @@ -88,27 +54,18 @@ admin_mode: - "FENCE_GATE" - "BUTTON" - "LEVER" - - "ENDER_CHEST" - "CRAFTING_TABLE" - "ENCHANTING_TABLE" - "ANVIL" - "BREWING_STAND" - - # Разрешенные блоки для постановки (whitelist) - # Если список пустой - можно ставить любые блоки - # Если список заполнен - можно ставить только эти блоки + # Whitelist блоков для постановки (пусто = любые) allowed_place_blocks: - - # Выдавать пресет блоков при входе в админ мод give_preset_blocks: false - - # Пресет блоков (формат: МАТЕРИАЛ:КОЛИЧЕСТВО) preset_blocks: - "BARRIER:64" - "LIGHT:64" - "STRUCTURE_VOID:64" - - # Сообщения + messages: enabled: "✓ Режим администратора активирован!" enabled_subtitle: "Ваше состояние сохранено." @@ -125,12 +82,11 @@ admin_mode: groups: base: "moder" - soft: "soft_moder_adm" - hard: "hard_moder_adm" + admin: "admin_moder" + chief: "chief_moder" items: - soft_panel_material: COMPASS - hard_panel_material: NETHER_STAR + panel_material: NETHER_STAR cp_inspector_material: STICK coreprotect: @@ -140,12 +96,33 @@ coreprotect: messages: prefix: "§8[§bAdminMode§8] " - enter_soft: "§aВошёл в режим §bSOFT MODER" - enter_hard: "§aВошёл в режим §cHARD MODER" + enter: "§aВошёл в §cADMIN MODE" exit: "§7Режим деактивирован. Состояние восстановлено." - cp_unavailable: "§cCoreProtect не найден. Hard режим работает без CP." + cp_unavailable: "§cCoreProtect не найден или отключён." session_recovered: "§eВаша активная сессия модерации была восстановлена и корректно завершена." logging: enabled: true log_file: "logs/sessions.log" + +moderation_actions: + dispatch_as_console: true + durations: + # Варианты, отображаемые в Dialog. permanent показывается как Permanently. + options: + - "1h" + - "1d" + - "7d" + - "30d" + - "permanent" + defaults: + warn: "7d" + mute: "1h" + ban: "7d" + commands: + warn: "warn {target} {duration} {reason}" + mute: "mute {target} {duration} actor:{moderator} {reason}" + # Namespace исключает конфликт с одноимённой командой FlectonePulse. + unmute: "moderationcommands:unmute {target} actor:{moderator} {reason}" + ban: "ban {target} {duration} actor:{moderator} {reason}" + kick: "kick {target} actor:{moderator} {reason}" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml old mode 100644 new mode 100755 index db70652..da47eda --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: AdminMode -version: '1.21.11' +version: '${version}' main: org.blz.adminmode.AdminMode -api-version: '1.21' +api-version: '26.1.2' author: BelzeBool, Kilka_v_HJIebe description: Плагин режима администратора с сохранением состояния игрока depend: [LuckPerms] @@ -10,7 +10,7 @@ softdepend: [CoreProtect] commands: adminmode: description: Режим администратора с сохранением состояния - usage: / + usage: / [on|off|toggle|dialog|disable|restrict|unrestrict|status|reload] aliases: [adm, am] admfix: description: Отключить все способности у игрока (полет, бессмертие) @@ -20,15 +20,36 @@ permissions: adminmode.use: description: Разрешает использовать режим администратора default: op - adminmode.profile.soft: - description: Разрешает использовать профиль soft_moder - default: op - adminmode.profile.hard: - description: Разрешает использовать профиль hard_moder - default: op adminmode.reload: description: Разрешает перезагружать конфигурацию AdminMode default: op adminmode.admin: - description: Разрешает использовать сильные действия и админ команды (disable, admfix) + description: Разрешает использовать сильные действия и команды (disable, admfix) default: op + adminmode.chief: + description: Разрешает главному модератору ограничивать доступ к admin mode + default: op + adminmode.punishment.warn: + description: Разрешает выдавать предупреждения через AdminMode + default: op + adminmode.punishment.unwarn: + description: Разрешает снимать предупреждения через AdminMode + default: op + adminmode.punishment.mute: + description: Разрешает выдавать муты через AdminMode + default: op + adminmode.punishment.unmute: + description: Разрешает снимать муты через AdminMode + default: op + adminmode.punishment.kick: + description: Разрешает кикать игроков через AdminMode + default: op + adminmode.punishment.ban: + description: Разрешает банить игроков через AdminMode + default: op + adminmode.punishment.unban: + description: Разрешает снимать баны через AdminMode + default: op + adminmode.special.belzebool: + description: Разрешает специальные способности BelzeBool + default: false diff --git a/src/main/resources/pocket_dimension.zip b/src/main/resources/pocket_dimension.zip old mode 100644 new mode 100755