Compare commits

...

233 Commits

Author SHA1 Message Date
ec2d243b7b fixed bug in HotBarRefill when using Blocks 2025-07-19 22:32:47 +02:00
ef6f34c2b2 added interaction sounds to settings 2025-07-19 16:36:31 +02:00
977f4ff4ec project dependency update 2025-07-19 16:27:39 +02:00
337727b0f0 fixed bug in FightDetector 2025-07-19 15:57:58 +02:00
44dae51e1c fixed playtimer 2025-06-24 21:23:05 +02:00
035864631d changed behavior to spawn in survival mode 2025-06-24 20:09:36 +02:00
f3b884058e code cleanup shrinkingborder 2025-06-23 20:34:48 +02:00
03d4f4e6d8 fixed bug in ShrinkingBorder 2025-06-23 20:28:13 +02:00
7422a89d98 fixed bug in fight detector 2025-06-23 19:52:30 +02:00
3590a5d278 finalized strikesystem 2025-06-22 14:20:45 +02:00
15ac47b314 auto playtime increment 2025-06-22 11:59:46 +02:00
af644a71ee ticketing enable and disable 2025-06-22 11:57:46 +02:00
0ce69f207f fixed bugs in strike handling 2025-06-22 10:59:38 +02:00
76297bb3af WIP: basic strike handling 2025-06-22 10:34:27 +02:00
1aad8f07c4 various bugfixes 2025-06-21 23:16:30 +02:00
f26f4ed56a cleanup 2025-06-21 21:35:31 +02:00
831eacaf47 added verbose logging for api requests
added autostrike for early leave
2025-06-21 21:22:49 +02:00
c71a2567bd fixed adminmarker handling api data wrong 2025-06-21 20:18:32 +02:00
72e88ce491 added spawnpoint for varo 2025-06-21 18:51:37 +02:00
66d84f4677 projectstart for varo 2025-06-21 18:15:25 +02:00
427aed9a7e fixed bug in teamtasks 2025-06-21 17:55:52 +02:00
0d1e6070ce updated playtimer and teamtasks 2025-06-21 17:18:47 +02:00
220fb9e229 moved existing spawning behavior to craftattack 2025-06-21 11:41:13 +02:00
9acac488f2 added api for querying admin-players 2025-06-21 11:38:09 +02:00
d71c0d768e configured shrinkingBorder for production use 2025-06-21 11:31:16 +02:00
9ef4c2e96b added playtimer ticket api 2025-06-20 17:07:53 +02:00
5d33d2aff7 updated adminmarker 2025-06-20 14:29:46 +02:00
3f1065fd3a added teamlist command 2025-06-19 23:49:48 +02:00
aa868deeca added team task management 2025-06-19 21:41:43 +02:00
b6c298cec3 unlimited admin access 2025-06-19 01:18:14 +02:00
8f5a96dc31 changed report text 2025-06-19 00:54:40 +02:00
2824c1053b WIP: report implementation for varo 2025-06-19 00:40:49 +02:00
ccf383cdb5 fixed configuration file not saving correctly 2025-06-15 18:55:17 +02:00
fce9449b7e implemented PlayTimer 2025-06-15 18:42:49 +02:00
69e971f618 Teams corrections
full implementation of FightDetector
2025-06-11 21:36:22 +02:00
b1f188dece generic tweaks
started implementation of FightDetector
2025-06-09 13:52:39 +02:00
a4289d5ac9 periodic team fetch 2025-05-30 22:00:42 +02:00
1fef363c50 Merge remote-tracking branch 'origin/master' 2025-05-30 18:35:14 +02:00
558e6f84f1 api header support, team api integration 2025-05-30 18:35:11 +02:00
bdbb8b5824 api header support, team api integration 2025-05-30 18:34:49 +02:00
8093a4a644 various changes for team management 2025-05-30 12:44:48 +02:00
50147a06e2 added removal of forbidden items in containers 2025-04-13 20:52:03 +02:00
a52476650e registered missing listener for DisplayName 2025-04-12 20:42:32 +02:00
0e5e841527 Merge branch 'master-shrinkingBorder' 2025-04-11 20:44:00 +02:00
ea5279dd82 Merge branch 'master-netherPrevent' 2025-04-11 20:43:08 +02:00
32cbbe6c51 made displayName independent of other appliances 2025-04-11 19:16:13 +02:00
9544c953a2 fixed shrinking border warning 2025-04-09 23:05:46 +02:00
34df173940 changed integer setting constructor to correctly use maximum 2025-04-09 22:32:36 +02:00
ca99e6cfef added integer setting 2025-04-09 16:50:29 +02:00
b0414ae6ab added warning with corresponding setting 2025-04-09 00:08:01 +02:00
c28d34ab88 added shrinking border to config 2025-04-08 19:03:22 +02:00
9bae26044a added shrinking border appliance 2025-04-08 19:00:49 +02:00
d1b5d81fa7 moved tablist to common and made project title configurable 2025-04-08 15:10:23 +02:00
e37e410542 moved report appliance to common 2025-04-08 15:04:50 +02:00
956d2717d8 Merge remote-tracking branch 'origin/master' 2025-04-08 11:49:31 +02:00
ef7232e687 fixed missing countdown for JoinProtection 2025-04-08 11:49:25 +02:00
ff31215295 added option for end prevent 2025-04-07 23:48:06 +02:00
a4a254ebbe removed unnecessary listeners 2025-04-07 22:33:01 +02:00
71d9faa9f4 added nether prevent 2025-04-07 22:17:54 +02:00
859733e3dd finalized JoinProtection 2025-04-07 19:15:35 +02:00
d94bbb7417 Merge branch 'master' into master-joinProtection 2025-04-07 17:55:47 +02:00
153a968776 added entity and potion listeners 2025-04-07 17:52:48 +02:00
639d06b01d Merge branch 'master' into master-joinProtection 2025-04-07 16:20:14 +02:00
fdbb525870 added forbiddenItems appliance with material detection 2025-04-07 16:19:44 +02:00
fcc2abdc49 added option to disable internal plugin http server 2025-04-07 14:53:18 +02:00
a3729734cb fixed error when opening settings and not all setting categories were used 2025-04-07 14:46:14 +02:00
90b623ea07 added option for local build tasks 2025-04-07 12:42:08 +02:00
9f49f44075 added join protection 2025-04-07 00:39:28 +02:00
e9a8e83019 cleanup build.gradle files 2025-04-05 14:39:45 +02:00
7c81286feb updated plugin.yml 2025-04-04 23:03:57 +02:00
e7cf3caae8 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	core/src/main/java/eu/mhsl/craftattack/core/appliance/Appliance.java
2025-04-04 23:03:47 +02:00
8742f5f631 updated command registration for usage without static plugin.yml file 2025-04-04 23:02:50 +02:00
2c0e264ece updated command registration for usage without static plugin.yml file 2025-04-04 22:51:01 +02:00
4592d53d22 splittet project to craftattack and varo flavors 2025-04-04 21:35:07 +02:00
6d0913fa0c splittet project to core and common functionalities 2025-04-04 20:08:53 +02:00
71d5d8303d added lightning fire control 2025-03-15 23:37:24 +01:00
49eeb646ea using common interface instead of individual methods in displayname appliance 2025-03-15 21:54:18 +01:00
ceca038b27 code reformat 2025-03-15 21:41:07 +01:00
76ceb9ef79 removed public directive where possible to reduce number of global accessible classes 2025-03-15 21:38:55 +01:00
219879974c categorized appliances in groups 2025-03-15 21:20:57 +01:00
bd630ebb7a fixed tabcomplete on mute command 2025-02-01 23:01:48 +01:00
c56f318f1c fixed wrong formatting on playtime 2025-02-01 23:01:28 +01:00
4d98d7aa75 removed restart kick message to trigger limbo when used 2024-12-27 11:01:18 +01:00
619190d0ae changed acInform teleport action to grim spectate 2024-12-27 11:00:55 +01:00
06641c5d84 added chatmute 2024-12-27 10:59:56 +01:00
2a52177043 added support for sentences in acinform reports 2024-12-26 20:50:01 +01:00
fc067a2ae0 moved late integrity check to login flow 2024-12-26 20:49:24 +01:00
116a9c11a2 updated ac inform design 2024-12-26 01:56:40 +01:00
3f29ceb08f fixed not respawning at spawnpoint on player death 2024-12-25 23:34:23 +01:00
a33ee357e8 fixed acinform not working with floating point numbers 2024-12-25 23:06:15 +01:00
e36256d5be async whitelist check 2024-12-25 21:09:42 +01:00
0e3a54a1b9 made repository calls async 2024-12-25 16:12:07 +01:00
2e67b41b44 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/eu/mhsl/craftattack/spawn/appliances/spawnpoint/Spawnpoint.java
2024-12-25 00:37:31 +01:00
e4ac8f7a63 renamed spawnpoint key name 2024-12-25 00:37:12 +01:00
e89e9d2181 renamed spawnpoint key name 2024-12-25 00:35:09 +01:00
8faf0efd60 added spawnpoint 2024-12-25 00:32:54 +01:00
2f1aeb71ee disabled infobars join restore 2024-12-25 00:13:54 +01:00
6475a7b825 updated texts and coordinates 2024-12-24 11:23:19 +01:00
193d8d778f Merge remote-tracking branch 'origin/master' 2024-12-20 21:52:25 +01:00
38da5b1d34 updated coordinates 2024-12-20 21:52:22 +01:00
04e3ddb09f changed custom advancements datapack name 2024-12-20 21:49:04 +01:00
47db27a86e removed bedrock block from WorldMuseum 2024-12-20 19:55:07 +01:00
f13534da3f more robust error handling on Whitelist 2024-12-20 19:54:38 +01:00
e45698c88a fix swapped command feedback 2024-12-20 18:46:20 +01:00
9197840873 removed unwanted HotbarRefill message 2024-12-20 13:34:32 +01:00
63d8335b3a reformatted code 2024-12-17 13:58:41 +01:00
184617e9c3 added EventRepository 2024-12-16 00:30:38 +01:00
696c4bc260 added bedrock displayname prefix 2024-12-09 22:04:30 +01:00
9004609c1b better synchronous call warning 2024-12-08 22:36:11 +01:00
ddedcea8ea added FeedbackRepository 2024-12-08 22:27:54 +01:00
318a30fe54 Merge branch 'refs/heads/master' into develop-apiUtil
# Conflicts:
#	src/main/java/eu/mhsl/craftattack/spawn/appliances/report/Report.java
2024-12-08 13:10:27 +01:00
4808d22f6a Merge remote-tracking branch 'origin/develop-apiUtil' into develop-apiUtil
# Conflicts:
#	src/main/java/eu/mhsl/craftattack/spawn/api/client/repositories/ReportRepository.java
#	src/main/java/eu/mhsl/craftattack/spawn/appliances/report/Report.java
2024-12-05 23:10:05 +01:00
6b2a323a9c implemented report repository 2024-12-05 23:02:15 +01:00
694ca0efba implemented reports request in Report.java 2024-12-05 22:16:15 +01:00
0276763a8d started implementing report repository 2024-12-05 18:58:55 +01:00
8811328571 implemented working WhitelistRepository 2024-12-05 12:46:51 +01:00
b3c43f1763 WIP: repositoryLoader and infrastructure 2024-12-04 23:34:12 +01:00
86677c942f implemented repository design pattern 2024-12-04 22:11:28 +01:00
31581fc643 added api util 2024-12-04 09:23:20 +01:00
a412f5c24c Merge remote-tracking branch 'refs/remotes/origin/master' into develop-feedback 2024-12-03 20:38:06 +01:00
eae979ee65 added feedback 2024-12-03 20:37:14 +01:00
28b9b84e07 changed afk timings map and time limit 2024-12-01 21:40:52 +01:00
a5cdb93f1b added /reports command 2024-12-01 18:03:33 +01:00
1572096020 Merge remote-tracking branch 'refs/remotes/origin/master' into develop-feedback 2024-12-01 17:41:12 +01:00
0ff9512a7f added wait message for ComponentUtil 2024-12-01 17:39:51 +01:00
f0ffb3ad21 Merge remote-tracking branch 'refs/remotes/origin/master' into develop-feedback 2024-11-28 22:16:13 +01:00
3c82939120 removed unwanted logging 2024-11-28 21:24:10 +01:00
c9935637b9 removed inconsistent spacing in acinform 2024-11-28 21:22:28 +01:00
b35c5b9240 updated afk and sleep tag icons 2024-11-25 20:40:43 +01:00
39814dae05 added sleepTag 2024-11-24 22:44:48 +01:00
7e3b043c98 added afkTag 2024-11-24 21:13:25 +01:00
5324749a64 Merge remote-tracking branch 'origin/master' 2024-11-24 20:06:22 +01:00
b9f88bc4b2 extended infobars, added coloring 2024-11-24 20:06:18 +01:00
8470115a74 added endPrevent 2024-11-24 02:33:36 +01:00
0f976d2316 added infobars 2024-11-10 17:28:02 +01:00
c52207298e Merge remote-tracking branch 'origin/master' 2024-11-10 12:23:52 +01:00
fd902f4ec6 updated maintenance kick message 2024-11-10 12:23:44 +01:00
0a7052b6f5 added error message for missing player name AcInform 2024-11-06 10:29:20 +01:00
0ea2d4958b Merge remote-tracking branch 'origin/master' 2024-11-05 22:28:33 +01:00
e8534b42ac prevented players from using ac inform command 2024-11-05 22:28:11 +01:00
3abf5a95e8 updated maintenance command feedback 2024-11-05 21:24:06 +01:00
fc5b76290e Merge remote-tracking branch 'origin/master' 2024-11-05 21:23:48 +01:00
01d1926104 removed webserver shutdown 2024-11-05 21:23:43 +01:00
e7075140e3 added anticheat info command 2024-11-05 20:48:42 +01:00
125b604393 small refactoring and added missing async task 2024-10-20 14:24:12 +02:00
c468696537 started with feedback applience 2024-10-19 19:53:51 +02:00
bc84b06f0d added AdminChat 2024-10-19 18:11:19 +02:00
b1427ac90e added playertime command 2024-10-19 15:46:34 +02:00
918ee5ed00 updated interfering listener 2024-10-09 14:36:08 +02:00
77fbc12873 Merge pull request 'develop-chatReply' (#5) from develop-chatReply into master
Reviewed-on: #5
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2024-10-06 13:52:58 +00:00
14a33cae39 Merge branch 'master' into develop-chatReply 2024-10-06 13:52:18 +00:00
8462e43e89 replaced replyMapping.get(sender) with variable replyList 2024-10-06 15:50:17 +02:00
985b36ddc8 solved pr comments except making chatMessages an object variable 2024-10-06 15:23:37 +02:00
d69089a0eb cleaned up the code 2024-10-06 12:32:20 +02:00
346847d2b2 Merge pull request 'master-customAdvancements' (#4) from master-customAdvancements into master
Reviewed-on: #4
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2024-10-05 16:40:51 +00:00
e3b07aa62f solved pr comments 2024-10-05 18:37:04 +02:00
497c5ad749 too old conversations get deleted, added prefix, changed some logic 2024-10-02 23:37:45 +02:00
471cd8e610 finished logic for reply command 2024-10-02 22:27:16 +02:00
f96356b620 added choose options 2024-09-30 16:23:32 +02:00
d8259b79ae Merge remote-tracking branch 'origin/develop-chatReply' into develop-chatReply 2024-09-29 22:44:10 +02:00
26b9fd5e0f changed basic reply logic 2024-09-29 22:43:55 +02:00
9390d8c67b Merge remote-tracking branch 'refs/remotes/origin/master' into develop-chatReply
# Conflicts:
#	src/main/java/eu/mhsl/craftattack/spawn/Main.java
#	src/main/java/eu/mhsl/craftattack/spawn/util/statistics/NetworkMonitor.java
#	src/main/resources/config.yml
#	src/main/resources/plugin.yml
2024-09-29 12:45:36 +02:00
da33e6e592 added more custom advancements, added pending advancements 2024-09-28 23:48:52 +02:00
1aebae6cd5 added fleischerchest and pixelblock advancements 2024-09-28 15:43:14 +02:00
bf94e152c8 changed snowball knockback 2024-09-28 12:59:42 +02:00
3c1dea3451 Merge remote-tracking branch 'origin/master' 2024-09-27 23:43:12 +02:00
36fc89e915 added snowball knockback 2024-09-27 23:42:56 +02:00
7a15984f19 added snowball knockback 2024-09-27 23:35:01 +02:00
87870c96f7 added snowball knockback 2024-09-27 23:32:28 +02:00
70a1740644 fixed rainbowComponent not folding back on end of color spectrum, ensuring no sudden color switch 2024-09-27 18:16:42 +02:00
24fbd50c62 added appendWithSpace component util 2024-09-27 18:02:24 +02:00
05e88845be network monitor now disables itself when not being able to read system stats properly 2024-09-27 17:09:45 +02:00
e5ff3d36fa added reflection for appliance loading, extended logging and error handling 2024-09-27 11:07:33 +02:00
d66996bc73 added year rank 2024-09-26 20:16:25 +02:00
247dae0155 added OptionLinks which are shown to the client as 'Server Links' 2024-09-25 19:04:12 +02:00
d7bc440620 added tab completion for maintenance command 2024-09-25 17:52:41 +02:00
b2021d5815 added maintenance mode 2024-09-25 14:49:30 +02:00
f4da7e7674 small cleanup in settings 2024-09-24 21:49:06 +02:00
da8e421532 moved setting declaration to each owning appliance 2024-09-24 19:20:59 +02:00
7a97b1595e added option to disable specific appliances 2024-09-24 18:31:48 +02:00
aaad9fe7d8 show more info in technical tab view 2024-09-23 19:18:19 +02:00
f89a935c05 added texture pack selector 2024-09-17 22:11:59 +02:00
f49cca7f33 added glowing berries 2024-09-14 20:36:04 +02:00
e11b3fd7bc renamed eventHandlers to listeners 2024-09-14 20:35:14 +02:00
d6a3fe358d added system monitor to tablist 2024-08-31 16:31:02 +02:00
0959eb4aa5 moved appliance queries to base method 2024-08-31 15:06:46 +02:00
50e4192e32 fixed double doors opening when sneaking 2024-08-31 12:14:48 +02:00
d21f009f7d door knocking no longer triggers when destroying doors 2024-08-31 11:39:40 +02:00
825fed639c fixed portable crafting when placing crafting tables 2024-08-31 11:17:55 +02:00
077c40f29d added knocking doors 2024-08-31 00:43:53 +02:00
2d696dcdbd added double doors 2024-08-30 23:58:01 +02:00
577e99fcfc added basic reply 2024-08-28 19:32:07 +02:00
ebab4cbd34 added chat mentions, added setting categories 2024-08-25 19:26:14 +02:00
4c4975b0a4 fixed hotbarRefill in creative mode 2024-08-25 17:42:57 +02:00
1ef4f86a14 added multi bool setting and switched hotbar replace to it 2024-08-25 14:30:18 +02:00
f187a867af added auto hotbar refill 2024-08-24 19:32:35 +02:00
62703ffd12 added anti sign edit 2024-08-24 16:49:35 +02:00
772687b15d added autoshulker, added support for SelectSettings 2024-08-24 15:06:21 +02:00
a60b6265a8 added portable crafting 2024-08-24 11:31:15 +02:00
3dc25d63fd applied code styling 2024-08-24 11:02:28 +02:00
78ba105291 added linebreaks for settings descriptions 2024-08-24 10:56:08 +02:00
35b40262a4 expanded debug commands, added setting toggle for join and leave messages 2024-08-24 02:46:50 +02:00
70058c552d added settings with technicalTab toggle 2024-08-24 01:37:02 +02:00
c01ae32f1f added networking statistics, refactored aggregates back to appliances 2024-08-23 23:17:02 +02:00
eb2c0505f5 added graceful stop for builtin webserver 2024-07-28 01:26:20 +02:00
7678fe11a3 Merge remote-tracking branch 'origin/master' 2024-07-28 01:26:07 +02:00
b5bab4e4b6 updated tablist 2024-07-28 01:25:54 +02:00
10918e284e added custom advancements appliance 2024-07-22 19:39:35 +02:00
c1cfb0c856 added Fleischerchest 2024-07-20 22:32:25 +02:00
71c413673d Added Outlaw change timeout 2023-12-30 00:36:36 +01:00
a3fe1bf737 Event advertisements and bugfixes 2023-12-29 01:30:29 +01:00
53b94c99a9 Fixed reward duplication on join 2023-12-27 10:00:26 +01:00
de559df98c Wide variety of changes for release 2023-12-27 01:15:04 +01:00
4bfcc5a2ff Added meta response structure for http API 2023-12-18 14:14:33 +01:00
2be1865263 Added HTTP API and Event Item Rewards 2023-12-10 21:34:23 +01:00
9278af6071 Changed wrong command syntax for reports 2023-12-10 15:57:02 +01:00
e16cf54712 Print out current playerLimit 2023-12-10 15:55:56 +01:00
f1e917ab7c Fixed bedrock players not passing whitelist correctly 2023-12-10 15:46:44 +01:00
41152d7864 Added CancelShutdown broadcast message 2023-12-10 14:50:07 +01:00
933ea496c8 Added Kick and PanicBan 2023-12-10 14:29:08 +01:00
c059880ece Cleanup and refactoring 2023-12-10 00:22:14 +01:00
0ea9738867 Added Whitelist checks 2023-12-09 21:21:22 +01:00
b72a5947b4 Moved BlockCycle logic to generic class 2023-12-09 00:09:46 +01:00
848c3f02fe Updated Projectstart to use generic Countdown 2023-12-08 23:56:33 +01:00
bbaf4f394b Added Scheduled server restarts 2023-12-08 22:44:24 +01:00
33e9e749a0 Added IteratorUtil 2023-12-08 22:44:11 +01:00
40d0950d22 Added floodgate tools 2023-12-08 22:43:25 +01:00
e651b8c799 Updated report tablist info 2023-12-08 21:50:18 +01:00
7c76177cfb Updated countdown to contain music and effetcs 2023-11-25 17:45:02 +01:00
efd228edfc Updated countdown design and behavior 2023-11-18 18:19:02 +01:00
5786b2409e Added Event api communication for creation of rooms 2023-11-18 15:27:50 +01:00
281a2109e6 Added base for whitelist 2023-11-18 15:27:24 +01:00
9d79e8dc07 Added player limiter 2023-11-18 15:27:07 +01:00
278 changed files with 11431 additions and 1420 deletions

2
.gitignore vendored
View File

@ -118,3 +118,5 @@ run/
!gradle-wrapper.jar !gradle-wrapper.jar
/gradlew /gradlew
/gradlew.bat /gradlew.bat
local.gradle

View File

@ -1,47 +1,53 @@
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'java' id 'java'
id 'com.gradleup.shadow' version "8.3.5"
} }
group = 'eu.mhsl.craftattack' allprojects {
version = '1.0' group = 'de.mhsl.craftattack'
version = '1.0.0'
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven {
name = "papermc-repo" name = "papermc-repo"
url = "https://repo.papermc.io/repository/maven-public/" url = "https://repo.papermc.io/repository/maven-public/"
} }
maven { maven {
name = "sonatype" name = "sonatype"
url = "https://oss.sonatype.org/content/groups/public/" url = "https://oss.sonatype.org/content/groups/public/"
}
maven {
url = uri("https://repo.opencollab.dev/main/")
}
} }
} }
dependencies { subprojects {
compileOnly "io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT" apply plugin: 'java'
implementation 'com.squareup.okhttp3:okhttp:4.11.0' apply plugin: 'com.gradleup.shadow'
}
def targetJavaVersion = 17 java {
java { toolchain {
def javaVersion = JavaVersion.toVersion(targetJavaVersion) languageVersion = JavaLanguageVersion.of(21)
sourceCompatibility = javaVersion }
targetCompatibility = javaVersion
if (JavaVersion.current() < javaVersion) {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
} }
} }
tasks.withType(JavaCompile).configureEach { configurations {
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { shadowImplementation.extendsFrom implementation
options.release = targetJavaVersion
}
} }
tasks.register('copyJarToServer', Exec) { shadowJar {
dependsOn jar configurations = [project.configurations.shadowImplementation]
mustRunAfter jar archiveClassifier.set('')
commandLine 'scp', 'build/libs/spawn-1.0.jar', 'root@10.20.6.1:/root/server/plugins' relocate 'org.apache.httpcomponents', 'eu.mhsl.lib.shadow.httpclient'
relocate 'com.sparkjava', 'eu.mhsl.lib.shadow.spark-core'
mergeServiceFiles()
}
if (file("local.gradle").exists()) {
apply from: "local.gradle"
} }

8
common/build.gradle Normal file
View File

@ -0,0 +1,8 @@
dependencies {
implementation project(':core')
compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT'
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4'
}

View File

@ -0,0 +1,28 @@
package eu.mhsl.craftattack.spawn.common.api;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.apache.http.client.utils.URIBuilder;
import org.bukkit.configuration.ConfigurationSection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
public class CraftAttackApi {
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("api"));
public final static String basePath = apiConfig.getString("baseurl");
public final static String apiSecret = apiConfig.getString("secret");
public static URI getBaseUri() {
Objects.requireNonNull(basePath);
try {
return new URI(basePath);
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
public static void withAuthorizationSecret(URIBuilder builder) {
builder.addParameter("secret", apiSecret);
}
}

View File

@ -0,0 +1,28 @@
package eu.mhsl.craftattack.spawn.common.api;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpRequest;
import java.util.Objects;
public class VaroApi {
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("varoApi"));
public final static String basePath = apiConfig.getString("endpoint");
public final static String apiSecret = apiConfig.getString("auth");
public static URI getBaseUri() {
Objects.requireNonNull(basePath);
try {
return new URI(basePath);
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
public static void authorizationHeader(HttpRequest.Builder builder) {
builder.header("Authorization", apiSecret);
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.common.api.repositories;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.util.UUID;
public class CraftAttackReportRepository extends ReportRepository {
public CraftAttackReportRepository() {
super(CraftAttackApi.getBaseUri(), new RequestModifier(CraftAttackApi::withAuthorizationSecret, null));
}
public ReqResp<PlayerReports> queryReports(UUID player) {
return this.get(
"report",
(parameters) -> parameters.addParameter("uuid", player.toString()),
PlayerReports.class
);
}
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post(
"report",
data,
ReportUrl.class
);
}
}

View File

@ -0,0 +1,42 @@
package eu.mhsl.craftattack.spawn.common.api.repositories;
import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
import eu.mhsl.craftattack.spawn.core.api.client.RepositoryLoader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.util.List;
import java.util.UUID;
@RepositoryLoader.Abstraction
public abstract class ReportRepository extends HttpRepository {
public ReportRepository(URI basePath, RequestModifier... baseRequestModifier) {
super(basePath, baseRequestModifier);
}
public record ReportCreationInfo(@NotNull UUID reporter, @Nullable UUID reported, String reason) {
}
public record ReportUrl(@NotNull String url) {
}
public record PlayerReports(
List<Report> from_self,
Object to_self
) {
public record Report(
@Nullable Reporter reported,
@NotNull String subject,
boolean draft,
@NotNull String status,
@NotNull String url
) {
public record Reporter(
@NotNull String username,
@NotNull String uuid
) {
}
}
}
}

View File

@ -0,0 +1,45 @@
package eu.mhsl.craftattack.spawn.common.api.repositories;
import eu.mhsl.craftattack.spawn.common.api.VaroApi;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class VaroReportRepository extends ReportRepository {
public VaroReportRepository() {
super(VaroApi.getBaseUri(), new RequestModifier(null, VaroApi::authorizationHeader));
}
public ReqResp<PlayerReports> queryReports(UUID player) {
throw new NotImplementedException("Report querying is not supported in Varo!");
}
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post(
"report",
data,
ReportUrl.class
);
}
public record StrikeCreationInfo(
@Nullable UUID reporter, // null for automatic creations
@NotNull UUID reported,
@NotNull String reason,
@Nullable String body,
@Nullable String notice,
@Nullable String statement,
int strike_reason_id // internal strike mapping
) {
}
public ReqResp<Void> createStrike(StrikeCreationInfo data) {
return this.put(
"report",
data,
Void.class
);
}
}

View File

@ -0,0 +1,20 @@
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command.AppliancesCommand;
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command.UserInfoCommand;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Debug extends Appliance {
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new UserInfoCommand(),
new AppliancesCommand()
);
}
}

View File

@ -0,0 +1,65 @@
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.Debug;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AppliancesCommand extends ApplianceCommand<Debug> {
public AppliancesCommand() {
super("appliances");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
ComponentBuilder<TextComponent, TextComponent.Builder> componentBuilder = Component.text()
.append(Component.text(Main.instance().getAppliances().size()))
.append(Component.text(" appliances running:"))
.appendNewline();
Main.instance().getAppliances().forEach(appliance -> {
List<ApplianceCommand<?>> commands = appliance.getCommands();
List<Listener> listener = appliance.getListeners();
componentBuilder
.append(Component.text(appliance.getClass().getSimpleName(), NamedTextColor.GREEN)
.hoverEvent(HoverEvent.showText(Component.text(appliance.getClass().getName()))))
.append(Component.text(": ", NamedTextColor.DARK_GRAY))
.append(Component.text(commands.size() + " Commands", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(commands.stream()
.map(applianceCommand -> Component.text()
.append(Component.text(applianceCommand.commandName, NamedTextColor.DARK_GREEN))
.append(Component.text(": "))
.append(Component.text(applianceCommand.getClass().getName()))
.build())
.reduce(ComponentUtil::appendWithNewline)
.orElse(Component.text("No commands available")))))
.append(Component.text(", ", NamedTextColor.GRAY))
.append(Component.text(listener.size() + " Listener", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(listener.stream()
.map(eventHandler -> Component.text()
.append(Component.text(eventHandler.getClass().getSimpleName(), NamedTextColor.DARK_GREEN))
.append(Component.text(": "))
.append(Component.text(eventHandler.getClass().getName()))
.build())
.reduce(ComponentUtil::appendWithNewline)
.orElse(Component.text("No listeners available")))))
.appendNewline();
});
componentBuilder.append(Component.text(Main.instance().getClass().getName(), NamedTextColor.GRAY));
sender.sendMessage(componentBuilder.build());
}
}

View File

@ -0,0 +1,82 @@
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.Debug;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Stream;
public class UserInfoCommand extends ApplianceCommand<Debug> {
public UserInfoCommand() {
super("userInfo");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length != 1) {
sender.sendMessage(Component.text("Bitte gib einen Nutzernamen an.", NamedTextColor.RED));
return;
}
OfflinePlayer player = Bukkit.getOfflinePlayer(args[0]);
sender.sendMessage(
Component.text()
.appendNewline()
.append(Component.text("Informationen zu: ", NamedTextColor.GOLD))
.append(
Component
.text(Objects.requireNonNull(player.getName()), NamedTextColor.YELLOW)
.clickEvent(ClickEvent.copyToClipboard(Objects.requireNonNull(player.getName())))
)
.appendNewline()
.append(
Component
.text("UUID: " + player.getUniqueId(), NamedTextColor.GRAY)
.clickEvent(ClickEvent.copyToClipboard(player.getUniqueId().toString()))
)
.appendNewline()
.append(
Component
.text("Erster Besuch: " + this.formatUnixTimestamp(player.getFirstPlayed()), NamedTextColor.GRAY)
.clickEvent(ClickEvent.copyToClipboard(String.valueOf(player.getFirstPlayed())))
)
.appendNewline()
.append(
Component
.text("Letzter Besuch: " + this.formatUnixTimestamp(player.getLastSeen()), NamedTextColor.GRAY)
.clickEvent(ClickEvent.copyToClipboard(String.valueOf(player.getLastSeen())))
)
.appendNewline()
);
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length < 2) {
return Stream.concat(
Bukkit.getOnlinePlayers().stream().map(Player::getName),
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
).toList();
}
return new ArrayList<>();
}
private String formatUnixTimestamp(long timestamp) {
DateFormat format = new SimpleDateFormat("E dd.MM.yyyy H:m:s");
return format.format(new Date(timestamp));
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.common.appliances.internal.titleClear;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class TitleClear extends Appliance {
public void clearTitle(Player player) {
player.clearTitle();
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new TitleClearListener()
);
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.common.appliances.internal.titleClear;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class TitleClearListener extends ApplianceListener<TitleClear> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
this.getAppliance().clearTitle(event.getPlayer());
}
}

View File

@ -0,0 +1,34 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.adminMarker;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName.DisplayName;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class AdminMarker extends Appliance implements DisplayName.Colored {
public final static String adminPermission = "admin";
@Override
public @Nullable TextColor getNameColor(Player player) {
if(player.hasPermission(adminPermission))
return TextColor.color(Color.AQUA.asRGB()); // TODO read permission from config
return TextColor.color(Color.WHITE.asRGB());
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
apiBuilder.get("isAdmin", request -> {
OfflinePlayer player = Bukkit.getOfflinePlayer(UUID.fromString(request.queryParams("player")));
Main.logger().info(String.format("Adminstatus requested for %s, response: %s", player.getUniqueId(), player.isOp()));
return player.isOp();
});
}
}

View File

@ -0,0 +1,66 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMention;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.sound.Sound;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class ChatMention extends Appliance {
private static List<String> playerNames;
public String formatPlayer(String name) {
return "@" + name;
}
public List<String> getPlayerNames() {
return playerNames;
}
public void refreshPlayers() {
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> playerNames = Arrays.stream(Bukkit.getOfflinePlayers())
.map(OfflinePlayer::getName)
.filter(Objects::nonNull)
.toList()
);
}
public void notifyPlayers(List<String> playerNames) {
playerNames.stream()
.distinct()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.filter(player -> Settings.instance()
.getSetting(player, Settings.Key.ChatMentions, ChatMentionSetting.ChatMentionConfig.class)
.notifyOnMention()
)
.forEach(player -> player.playSound(
Sound.sound(
org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP,
Sound.Source.PLAYER,
1.0f,
1.0f
)
));
}
@Override
public void onEnable() {
Settings.instance().declareSetting(ChatMentionSetting.class);
this.refreshPlayers();
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new ChatMentionListener());
}
}

View File

@ -0,0 +1,56 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMention;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMessages.ChatMessages;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import io.papermc.paper.event.player.AsyncChatDecorateEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
import java.util.ArrayList;
import java.util.List;
class ChatMentionListener extends ApplianceListener<ChatMention> {
@SuppressWarnings("UnstableApiUsage")
@EventHandler
public void coloringEvent(AsyncChatDecorateEvent event) {
String message = PlainTextComponentSerializer.plainText().serialize(event.result());
List<String> words = List.of(message.split(" "));
List<String> mentioned = new ArrayList<>();
ChatMentionSetting.ChatMentionConfig config = Settings.instance()
.getSetting(event.player(), Settings.Key.ChatMentions, ChatMentionSetting.ChatMentionConfig.class);
ChatMessages chatMessages = Main.instance().getAppliance(ChatMessages.class);
Component result = words.stream()
.map(word -> {
String wordWithoutAnnotation = word.replace("@", "");
boolean isPlayer = this.getAppliance().getPlayerNames().contains(wordWithoutAnnotation);
if(isPlayer && config.applyMentions()) {
mentioned.add(wordWithoutAnnotation);
Component mention = Component.text(
this.getAppliance().formatPlayer(wordWithoutAnnotation),
NamedTextColor.GOLD
);
return chatMessages.addReportActions(mention, wordWithoutAnnotation);
} else {
return Component.text(word);
}
})
.reduce(ComponentUtil::appendWithSpace)
.orElseThrow();
this.getAppliance().notifyPlayers(mentioned);
event.result(result);
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().refreshPlayers();
}
}

View File

@ -0,0 +1,49 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMention;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.MultiBoolSetting;
import org.bukkit.Material;
public class ChatMentionSetting extends MultiBoolSetting<ChatMentionSetting.ChatMentionConfig> implements CategorizedSetting {
@Override
public SettingCategory category() {
return SettingCategory.Visuals;
}
public record ChatMentionConfig(
@DisplayName("Spielernamen hervorheben") boolean applyMentions,
@DisplayName("Benachrichtigungston") boolean notifyOnMention
) {
}
public ChatMentionSetting() {
super(Settings.Key.ChatMentions);
}
@Override
protected String title() {
return "Erwähnungen im Chat";
}
@Override
protected String description() {
return "Erwähnungen werden automatisch im Chat angewandt und der Empfänger erhält einen Signalton";
}
@Override
protected Material icon() {
return Material.FEATHER;
}
@Override
protected ChatMentionConfig defaultValue() {
return new ChatMentionConfig(true, true);
}
@Override
public Class<?> dataType() {
return ChatMentionConfig.class;
}
}

View File

@ -0,0 +1,36 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMessages;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class ChatMessages extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(ShowJoinAndLeaveMessagesSetting.class);
}
public Component getReportablePlayerName(Player player) {
return this.addReportActions(player.displayName(), player.getName());
}
public Component addReportActions(Component message, String username) {
return message
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um diesen Spieler zu reporten").color(NamedTextColor.GOLD)))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, String.format("/report %s ", username)));
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new ChatMessagesListener());
}
}

View File

@ -0,0 +1,74 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMessages;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.util.IteratorUtil;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Color;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Optional;
class ChatMessagesListener extends ApplianceListener<ChatMessages> {
@EventHandler
public void onPlayerChatEvent(AsyncChatEvent event) {
event.renderer(
(source, sourceDisplayName, message, viewer) ->
Component.text("")
.append(this.getAppliance().getReportablePlayerName(source))
.append(Component.text(" > ").color(TextColor.color(Color.GRAY.asRGB())))
.append(message).color(TextColor.color(Color.SILVER.asRGB()))
);
}
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerJoin(PlayerJoinEvent event) {
boolean wasHidden = event.joinMessage() == null;
event.joinMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage(
Component
.text(">>> ").color(NamedTextColor.GREEN)
.append(this.getAppliance().getReportablePlayerName(event.getPlayer()))
);
});
}
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
boolean wasHidden = event.quitMessage() == null;
event.quitMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage(
Component
.text("<<< ").color(NamedTextColor.RED)
.append(this.getAppliance().getReportablePlayerName(event.getPlayer()))
);
});
}
@EventHandler
public void onDeath(PlayerDeathEvent event) {
event.deathMessage(
Component
.text("")
.append(
Optional
.ofNullable(event.deathMessage())
.orElse(Component.text(event.getPlayer().getName()))
)
.color(TextColor.color(Color.SILVER.asRGB()))
);
}
}

View File

@ -0,0 +1,38 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMessages;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class ShowJoinAndLeaveMessagesSetting extends BoolSetting implements CategorizedSetting {
public ShowJoinAndLeaveMessagesSetting() {
super(Settings.Key.ShowJoinAndLeaveMessages);
}
@Override
protected String title() {
return "Join & Leave Nachrichten anzeigen";
}
@Override
protected String description() {
return "Zeige allgemeine Beitritts und Verlassensmeldungen im Chat";
}
@Override
protected Material icon() {
return Material.PLAYER_HEAD;
}
@Override
protected Boolean defaultValue() {
return true;
}
@Override
public SettingCategory category() {
return SettingCategory.Visuals;
}
}

View File

@ -0,0 +1,91 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
public class DisplayName extends Appliance {
public interface Prefixed {
@Nullable
Component getNamePrefix(Player player);
}
public interface Colored {
@Nullable
TextColor getNameColor(Player player);
}
public void update(Player player) {
List<Colored> coloring = Main.instance().getAppliances().stream()
.filter(appliance -> appliance instanceof Colored)
.map(appliance -> (Colored) appliance)
.toList();
if(coloring.size() > 1) throw new IllegalStateException(
"There are two or more appliances which provide coloring for player names. This is currently not supported!"
);
TextColor playerColor = coloring.isEmpty()
? NamedTextColor.WHITE
: coloring.getFirst().getNameColor(player);
List<Prefixed> prefixes = Main.instance().getAppliances().stream()
.filter(appliance -> appliance instanceof Prefixed)
.map(appliance -> (Prefixed) appliance)
.toList();
ComponentBuilder<TextComponent, TextComponent.Builder> playerName = Component.text();
prefixes.stream()
.map(prefixed -> prefixed.getNamePrefix(player))
.filter(Objects::nonNull)
.forEach(prefix -> playerName
.append(prefix)
.append(ComponentUtil.clearedSpace())
);
if(Floodgate.isBedrock(player)) {
playerName
.append(
Component.text("\uD83C\uDFAE", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(Component.text(
String.format("%s spielt die Minecraft: Bedrock Edition", player.getName())
)))
)
.append(ComponentUtil.clearedSpace());
}
playerName.append(Component.text(player.getName(), playerColor));
this.setGlobal(player, playerName.build());
}
private void setGlobal(Player player, Component component) {
try {
player.customName(component);
player.displayName(component);
player.playerListName(component);
} catch(Exception e) {
Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage);
}
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new DisplayNameUpdateListener());
}
}

View File

@ -0,0 +1,13 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerJoinEvent;
class DisplayNameUpdateListener extends ApplianceListener<DisplayName> {
@EventHandler(priority = EventPriority.LOW)
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().update(event.getPlayer());
}
}

View File

@ -0,0 +1,28 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.DiscordCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.HelpCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.SpawnCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.TeamspeakCommand;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Help extends Appliance {
public Help() {
super("help");
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new HelpCommand(),
new SpawnCommand(),
new TeamspeakCommand(),
new DiscordCommand()
);
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class DiscordCommand extends ApplianceCommand<Help> {
public DiscordCommand() {
super("discord");
}
private final static String discordLink = "https://discord.gg/TXxspGVanq";
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
sender.sendMessage(
Component.text("Offizieller Discord Server: ", NamedTextColor.GOLD)
.append(Component.text(discordLink, NamedTextColor.AQUA))
.clickEvent(ClickEvent.openUrl(discordLink))
);
}
}

View File

@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class HelpCommand extends ApplianceCommand<Help> {
public HelpCommand() {
super("help");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(
Component.text("Willkommen auf Craftattack!", NamedTextColor.GOLD)
.appendNewline()
.append(Component.text("Wenn du hilfe benötigst kannst du dich jederzeit an einen Admin wenden." +
" Weitere Informationen zu Funktionen und Befehlen erhältst du zudem im Turm am Spawn.", NamedTextColor.GRAY))
);
}
}

View File

@ -1,7 +1,7 @@
package eu.mhsl.craftattack.spawn.appliances.help.command; package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.appliances.help.Help; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
@ -12,13 +12,15 @@ import java.util.Objects;
public class SpawnCommand extends ApplianceCommand<Help> { public class SpawnCommand extends ApplianceCommand<Help> {
private static final String spawnKey = "spawn"; private static final String spawnKey = "spawn";
public SpawnCommand() { public SpawnCommand() {
super("spawn"); super("spawn");
} }
@Override @Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(!getAppliance().localConfig().isString(spawnKey)) throw new ApplianceCommand.Error("Es wurde kein Spawnbereich hinterlegt!"); if(!this.getAppliance().localConfig().isString(spawnKey))
sender.sendMessage(Component.text(Objects.requireNonNull(getAppliance().localConfig().getString(spawnKey)), NamedTextColor.GOLD)); throw new ApplianceCommand.Error("Es wurde kein Spawnbereich hinterlegt!");
sender.sendMessage(Component.text(Objects.requireNonNull(this.getAppliance().localConfig().getString(spawnKey)), NamedTextColor.GOLD));
} }
} }

View File

@ -1,7 +1,7 @@
package eu.mhsl.craftattack.spawn.appliances.help.command; package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.appliances.help.Help; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
@ -10,23 +10,25 @@ import org.jetbrains.annotations.NotNull;
public class TeamspeakCommand extends ApplianceCommand<Help> { public class TeamspeakCommand extends ApplianceCommand<Help> {
private static final String teamspeakKey = "teamspeak"; private static final String teamspeakKey = "teamspeak";
public TeamspeakCommand() { public TeamspeakCommand() {
super("teamspeak"); super("teamspeak");
} }
@Override @Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(!getAppliance().localConfig().isString(teamspeakKey)) throw new ApplianceCommand.Error("Es wurde kein Teamspeak hinterlegt!"); if(!this.getAppliance().localConfig().isString(teamspeakKey))
throw new ApplianceCommand.Error("Es wurde kein Teamspeak hinterlegt!");
sender.sendMessage( sender.sendMessage(
Component.text() Component.text()
.append(Component.text("Joine unserem Teamspeak: ", NamedTextColor.GOLD)) .append(Component.text("Joine unserem Teamspeak: ", NamedTextColor.GOLD))
.append(getTeamspeakIp(getAppliance().localConfig().getString(teamspeakKey))) .append(this.getTeamspeakIp(this.getAppliance().localConfig().getString(teamspeakKey)))
); );
} }
private Component getTeamspeakIp(String ip) { private Component getTeamspeakIp(String ip) {
return Component.text() return Component.text()
.append(Component.text(ip, NamedTextColor.AQUA)) .append(Component.text(ip, NamedTextColor.AQUA))
.build(); .build();
} }
} }

View File

@ -0,0 +1,69 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
import eu.mhsl.craftattack.spawn.core.Main;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
public abstract class Bar {
private BossBar bossBar;
private final BukkitTask updateTask;
public Bar() {
long refreshRateInTicks = this.refresh().get(ChronoUnit.SECONDS) * Ticks.TICKS_PER_SECOND;
this.updateTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
this::update,
refreshRateInTicks,
refreshRateInTicks
);
}
public BossBar getBossBar() {
if(this.bossBar == null) this.bossBar = this.createBar();
return this.bossBar;
}
private BossBar createBar() {
return BossBar.bossBar(
this.title(),
this.correctedProgress(),
this.color(),
this.overlay()
);
}
private void update() {
if(this.bossBar == null) return;
this.beforeRefresh();
this.bossBar.name(this.title());
this.bossBar.progress(this.correctedProgress());
this.bossBar.color(this.color());
this.bossBar.overlay(this.overlay());
}
public void stopUpdate() {
this.updateTask.cancel();
}
private float correctedProgress() {
return Math.clamp(this.progress(), 0, 1);
}
protected void beforeRefresh() {
}
protected abstract Duration refresh();
protected abstract String name();
protected abstract Component title();
protected abstract float progress();
protected abstract BossBar.Color color();
protected abstract BossBar.Overlay overlay();
}

View File

@ -0,0 +1,32 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
class InfoBarCommand extends ApplianceCommand.PlayerChecked<InfoBars> {
public InfoBarCommand() {
super("infobar");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length == 0) throw new Error("<show|hide|hideall> [bar name]");
switch(args[0]) {
case "hideAll" -> this.getAppliance().hideAll(this.getPlayer());
case "show" -> this.getAppliance().show(this.getPlayer(), args[1]);
case "hide" -> this.getAppliance().hide(this.getPlayer(), args[1]);
default -> throw new Error("Erlaubte Optionen sind 'show', 'hide', 'hideAll'!");
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 1) return List.of("show", "hide", "hideAll");
return this.getAppliance().getInfoBars().stream().map(Bar::name).toList();
}
}

View File

@ -0,0 +1,92 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.MsptBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.PlayerCounterBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsBar;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class InfoBars extends Appliance {
private final NamespacedKey infoBarKey = new NamespacedKey(Main.instance(), "infobars");
private final List<Bar> infoBars = List.of(
new TpsBar(),
new MsptBar(),
new PlayerCounterBar()
);
public void showAll(Player player) {
this.getStoredBars(player).forEach(bar -> this.show(player, bar));
}
public void hideAll(Player player) {
this.getStoredBars(player).forEach(bar -> this.hide(player, bar));
this.setStoredBars(player, List.of());
}
public void show(Player player, String bar) {
this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getStoredBars(player));
existingBars.add(bar);
player.showBossBar(this.getBarByName(bar).getBossBar());
this.setStoredBars(player, existingBars);
}
public void hide(Player player, String bar) {
this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getStoredBars(player));
existingBars.remove(bar);
player.hideBossBar(this.getBarByName(bar).getBossBar());
this.setStoredBars(player, existingBars);
}
private List<String> getStoredBars(Player player) {
PersistentDataContainer container = player.getPersistentDataContainer();
if(!container.has(this.infoBarKey)) return List.of();
return container.get(this.infoBarKey, PersistentDataType.LIST.strings());
}
private void setStoredBars(Player player, List<String> bars) {
player.getPersistentDataContainer().set(this.infoBarKey, PersistentDataType.LIST.strings(), bars);
}
private Bar getBarByName(String name) {
return this.infoBars.stream()
.filter(bar -> bar.name().equalsIgnoreCase(name))
.findFirst()
.orElse(null);
}
private void validateBarName(String name) {
if(this.getBarByName(name) == null)
throw new ApplianceCommand.Error(String.format("Ungültiger infobar name '%s'", name));
}
public List<Bar> getInfoBars() {
return this.infoBars;
}
@Override
public void onDisable() {
this.infoBars.forEach(Bar::stopUpdate);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new ShowPreviousBarsListener());
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new InfoBarCommand());
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class ShowPreviousBarsListener extends ApplianceListener<InfoBars> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
// this.getAppliance().showAll(event.getPlayer());
}
}

View File

@ -0,0 +1,57 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
import eu.mhsl.craftattack.spawn.core.util.statistics.ServerMonitor;
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import java.time.Duration;
public class MsptBar extends Bar {
@Override
protected Duration refresh() {
return Duration.ofSeconds(3);
}
@Override
protected String name() {
return "mspt";
}
@Override
protected Component title() {
return Component.text()
.append(Component.text("M"))
.append(Component.text("illi", NamedTextColor.GRAY))
.append(Component.text("S"))
.append(Component.text("econds ", NamedTextColor.GRAY))
.append(Component.text("P"))
.append(Component.text("er ", NamedTextColor.GRAY))
.append(Component.text("T"))
.append(Component.text("ick", NamedTextColor.GRAY))
.append(Component.text(": "))
.append(Component.text(String.format("%.2f", this.currentMSPT()), ColorUtil.msptColor(this.currentMSPT())))
.build();
}
@Override
protected float progress() {
return this.currentMSPT() / 50f;
}
@Override
protected BossBar.Color color() {
return BossBar.Color.BLUE;
}
@Override
protected BossBar.Overlay overlay() {
return BossBar.Overlay.PROGRESS;
}
private float currentMSPT() {
return ServerMonitor.getServerMSPT();
}
}

View File

@ -0,0 +1,56 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
import eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit.PlayerLimit;
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import java.time.Duration;
public class PlayerCounterBar extends Bar {
@Override
protected Duration refresh() {
return Duration.ofSeconds(3);
}
@Override
protected String name() {
return "playerCounter";
}
@Override
protected Component title() {
TextColor color = ColorUtil.mapGreenToRed(this.getCurrentPlayerCount(), 0, this.getMaxPlayerCount(), true);
return Component.text()
.append(Component.text("Spieler online: "))
.append(Component.text(this.getCurrentPlayerCount(), color))
.build();
}
@Override
protected float progress() {
return (float) this.getCurrentPlayerCount() / this.getMaxPlayerCount();
}
@Override
protected BossBar.Color color() {
return BossBar.Color.BLUE;
}
@Override
protected BossBar.Overlay overlay() {
return BossBar.Overlay.PROGRESS;
}
private int getCurrentPlayerCount() {
return Bukkit.getOnlinePlayers().size();
}
private int getMaxPlayerCount() {
return Main.instance().getAppliance(PlayerLimit.class).getLimit();
}
}

View File

@ -0,0 +1,55 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import java.time.Duration;
public class TpsBar extends Bar {
@Override
protected Duration refresh() {
return Duration.ofSeconds(3);
}
@Override
protected String name() {
return "tps";
}
@Override
protected Component title() {
return Component.text()
.append(Component.text("T"))
.append(Component.text("icks ", NamedTextColor.GRAY))
.append(Component.text("P"))
.append(Component.text("er ", NamedTextColor.GRAY))
.append(Component.text("S"))
.append(Component.text("econds", NamedTextColor.GRAY))
.append(Component.text(": "))
.append(Component.text(String.format("%.2f", this.currentTps()), ColorUtil.tpsColor(this.currentTps())))
.build();
}
@Override
protected float progress() {
return this.currentTps() / 20;
}
@Override
protected BossBar.Color color() {
return BossBar.Color.BLUE;
}
@Override
protected BossBar.Overlay overlay() {
return BossBar.Overlay.NOTCHED_20;
}
private float currentTps() {
return (float) Bukkit.getTPS()[0];
}
}

View File

@ -0,0 +1,194 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.chatMessages.ChatMessages;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.commands.PrivateMessageCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.commands.PrivateReplyCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
public class PrivateMessage extends Appliance {
public final int targetChangeTimeoutSeconds = 30;
public final int conversationTimeoutMinutes = 30;
private record Conversation(UUID target, Long lastSet) {
}
private final Map<Player, List<Conversation>> replyMapping = new WeakHashMap<>();
public void reply(Player sender, String message) {
this.replyMapping.computeIfAbsent(sender, player -> new ArrayList<>());
List<Conversation> replyList = this.replyMapping.get(sender);
List<Conversation> tooOldConversations = replyList.stream()
.filter(conversation -> conversation.lastSet < System.currentTimeMillis() - (this.conversationTimeoutMinutes * 60 * 1000))
.toList();
replyList.removeAll(tooOldConversations);
if(replyList.isEmpty()) throw new ApplianceCommand.Error("Du führst aktuell keine Konversation.");
Component privatePrefix = Component.text("[Privat] ", NamedTextColor.LIGHT_PURPLE);
Conversation youngestEntry = replyList.stream()
.max(Comparator.comparingLong(o -> o.lastSet))
.orElse(replyList.getLast());
if(message.isBlank()) {
Player currentTargetPlayer = Bukkit.getPlayer(youngestEntry.target());
Component currentTargetComponent = currentTargetPlayer != null
? Main.instance().getAppliance(ChatMessages.class).getReportablePlayerName(currentTargetPlayer)
: Component.text("niemandem.");
sender.sendMessage(
privatePrefix
.append(Component.text("Du schreibst aktuell mit ", NamedTextColor.GRAY))
.append(currentTargetComponent)
);
return;
}
List<Conversation> oldConversations = replyList.stream()
.filter(conversation -> conversation.lastSet < System.currentTimeMillis() - (this.targetChangeTimeoutSeconds * 1000))
.toList();
if(oldConversations.contains(youngestEntry) || replyList.size() == 1) {
Player target = Bukkit.getPlayer(youngestEntry.target());
if(target == null)
throw new ApplianceCommand.Error("Der Spieler " + Bukkit.getOfflinePlayer(youngestEntry.target()).getName() + " ist nicht mehr verfügbar.");
replyList.clear();
this.sendWhisper(sender, new ResolvedPmUserArguments(target, message));
return;
}
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text();
component.append(
Component.newline()
.append(privatePrefix)
.append(Component.text("Das Ziel für /r hat sich bei dir in den letzten ", NamedTextColor.RED))
.append(Component.text(String.valueOf(this.targetChangeTimeoutSeconds), NamedTextColor.RED))
.append(Component.text(" Sekunden geändert. Wer soll deine Nachricht erhalten? ", NamedTextColor.RED))
.appendNewline()
.appendNewline()
);
if(!oldConversations.isEmpty()) {
Conversation youngestOldConversation = oldConversations.stream()
.max(Comparator.comparingLong(o -> o.lastSet))
.orElse(oldConversations.getLast());
replyList.removeAll(oldConversations);
replyList.add(youngestOldConversation);
}
List<String> playerNames = replyList.stream()
.map(conversation -> Bukkit.getOfflinePlayer(conversation.target()).getName())
.distinct()
.toList();
playerNames.forEach(playerName -> component.append(
Component.text("[")
.append(Component.text(playerName, NamedTextColor.GOLD))
.append(Component.text("]"))
.clickEvent(ClickEvent.runCommand(String.format("/msg %s %s", playerName, message)))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um diesem Spieler zu schreiben.").color(NamedTextColor.GOLD))))
.append(Component.text(" "))
);
component.appendNewline();
sender.sendMessage(component.build());
}
public void sendWhisper(Player sender, ResolvedPmUserArguments userArguments) {
Conversation newReceiverConversation = new Conversation(
sender.getUniqueId(),
System.currentTimeMillis()
);
if(this.replyMapping.get(userArguments.receiver) != null) {
List<Conversation> oldEntries = this.replyMapping.get(userArguments.receiver).stream()
.filter(conversation -> conversation.target() == sender.getUniqueId())
.toList();
this.replyMapping.get(userArguments.receiver).removeAll(oldEntries);
} else {
this.replyMapping.put(
userArguments.receiver,
new ArrayList<>()
);
}
this.replyMapping.get(userArguments.receiver).add(newReceiverConversation);
List<Conversation> senderConversationList = new ArrayList<>();
senderConversationList.add(
new Conversation(
userArguments.receiver.getUniqueId(),
System.currentTimeMillis()
)
);
this.replyMapping.put(
sender,
senderConversationList
);
ChatMessages chatMessages = Main.instance().getAppliance(ChatMessages.class);
Component privatePrefix = Component.text("[Privat] ", NamedTextColor.LIGHT_PURPLE);
sender.sendMessage(
Component.text()
.append(privatePrefix.clickEvent(ClickEvent.suggestCommand(String.format("/msg %s ", userArguments.receiver.getName()))))
.append(sender.displayName())
.append(Component.text(" zu ", NamedTextColor.GRAY))
.append(chatMessages.getReportablePlayerName(userArguments.receiver))
.append(Component.text(" > ", NamedTextColor.GRAY))
.append(Component.text(userArguments.message))
);
userArguments.receiver.sendMessage(
Component.text()
.append(privatePrefix.clickEvent(ClickEvent.suggestCommand(String.format("/msg %s ", sender.getName()))))
.append(chatMessages.getReportablePlayerName(sender))
.append(Component.text(" zu ", NamedTextColor.GRAY))
.append(userArguments.receiver.displayName())
.append(Component.text(" > ", NamedTextColor.GRAY))
.append(Component.text(userArguments.message))
);
}
public record ResolvedPmUserArguments(Player receiver, String message) {
}
public ResolvedPmUserArguments resolveImplicit(String[] args) {
if(args.length < 2)
throw new ApplianceCommand.Error("Es muss ein Spieler sowie eine Nachricht angegeben werden.");
List<String> arguments = List.of(args);
Player targetPlayer = Bukkit.getPlayer(arguments.getFirst());
if(targetPlayer == null)
throw new ApplianceCommand.Error(String.format("Der Spieler %s konnte nicht gefunden werden.", arguments.getFirst()));
String message = arguments.stream().skip(1).collect(Collectors.joining(" "));
return new ResolvedPmUserArguments(targetPlayer, message);
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new PrivateMessageCommand(),
new PrivateReplyCommand()
);
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.commands;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.PrivateMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class PrivateMessageCommand extends ApplianceCommand.PlayerChecked<PrivateMessage> {
public PrivateMessageCommand() {
super("msg");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().sendWhisper(this.getPlayer(), this.getAppliance().resolveImplicit(args));
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.commands;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.privateMessage.PrivateMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class PrivateReplyCommand extends ApplianceCommand.PlayerChecked<PrivateMessage> {
public PrivateReplyCommand() {
super("r");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().reply(this.getPlayer(), String.join(" ", args));
}
}

View File

@ -0,0 +1,180 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.common.api.repositories.ReportRepository;
import eu.mhsl.craftattack.spawn.common.api.repositories.VaroReportRepository;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.repositories.CraftAttackReportRepository;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
public class Report extends Appliance {
public static Component helpText() {
return Component.text()
.appendNewline()
.append(Component.text(" Um einen Spieler zu melden, verwende ", NamedTextColor.GRAY)).appendNewline()
.append(Component.text("/report", NamedTextColor.GOLD)).appendNewline()
.append(Component.text("oder", NamedTextColor.GRAY)).appendNewline()
.append(Component.text("/report <spieler> [grund]", NamedTextColor.GOLD)).appendNewline()
.build();
}
public Report() {
super("report");
}
public void reportToUnknown(@NotNull Player issuer) {
CraftAttackReportRepository.ReportCreationInfo request = new CraftAttackReportRepository.ReportCreationInfo(issuer.getUniqueId(), null, "");
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.createReport(issuer, request)
);
}
public void reportToKnown(@NotNull Player issuer, @NotNull String targetUsername, @Nullable String reason) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(targetUsername);
if(issuer.getUniqueId().equals(offlinePlayer.getUniqueId())) {
issuer.sendMessage(Component.text("Du kannst dich nicht selbst reporten.", NamedTextColor.RED));
return;
}
ReportRepository.ReportCreationInfo request = new ReportRepository.ReportCreationInfo(
issuer.getUniqueId(),
offlinePlayer.getUniqueId(),
Optional.ofNullable(reason).orElse("")
);
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.createReport(issuer, request)
);
}
private void createReport(Player issuer, ReportRepository.ReportCreationInfo reportRequest) {
ReqResp<ReportRepository.ReportUrl> createdReport = this.queryRepository(VaroReportRepository.class)
.createReport(reportRequest);
switch(createdReport.status()) {
case 200: // varo-endpoint specific
case 201:
issuer.sendMessage(
Component.text()
.append(Component.text("\\/".repeat(20), NamedTextColor.DARK_GRAY))
.appendNewline()
.append(Component.text("⚠ Der Report muss über den folgenden Link fertiggestellt werden:", NamedTextColor.GOLD))
.appendNewline()
.appendNewline()
.append(
Component
.text(createdReport.data().url(), NamedTextColor.GRAY) // URL mit Weltkugel-Emoji
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url()))
)
.appendNewline()
.appendNewline()
.append(Component.text("Ohne das Fertigstellen des Reports wird dieser nicht bearbeitet!", NamedTextColor.DARK_RED))
.appendNewline()
.append(Component.text("/\\".repeat(20), NamedTextColor.DARK_GRAY))
);
break;
case 400:
issuer.sendMessage(
Component.text()
.append(Component.text("Der angegebene Nutzer ist in unserem System nicht bekannt.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bist du sicher, dass du den Namen richtig geschrieben hast?", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Du kannst dich alternativ jederzeit bei einem Admin melden.", NamedTextColor.GRAY))
);
break;
case 401:
default:
Main.logger().warning("Failed to request Report: " + createdReport.status());
issuer.sendMessage(
Component.text()
.append(Component.text("Interner Serverfehler beim anlegen des Reports.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
);
break;
}
}
public void queryReports(Player issuer) {
ReqResp<ReportRepository.PlayerReports> userReports = this.queryRepository(VaroReportRepository.class)
.queryReports(issuer.getUniqueId());
if(userReports.status() != 200) {
Main.logger().warning("Failed to request Reports: " + userReports.status());
issuer.sendMessage(
Component.text()
.append(Component.text("Interner Serverfehler beim abfragen der Reports.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
);
return;
}
List<ReportRepository.PlayerReports.Report> reports = userReports
.data()
.from_self()
.stream()
.filter(report -> !report.draft())
.toList()
.reversed();
if(reports.isEmpty()) {
issuer.sendMessage(
Component.text()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY))
);
return;
}
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.newline())
.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD))
.appendNewline();
reports.forEach(report -> {
component
.append(Component.text(" - ", NamedTextColor.WHITE))
.append(
report.reported() != null
? Component.text(report.reported().username(), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW)
)
.append(Component.text(String.format(": %s", report.subject()), NamedTextColor.GRAY))
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um den Report einzusehen.", NamedTextColor.GOLD)));
component.appendNewline();
});
issuer.sendMessage(component.build());
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new ReportCommand(),
new ReportsCommand()
);
}
}

View File

@ -0,0 +1,65 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
public ReportCommand() {
super("report");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(ComponentUtil.pleaseWait());
if(args.length == 0) {
this.getAppliance().reportToUnknown(this.getPlayer());
}
if(args.length == 1) {
this.getAppliance().reportToKnown(this.getPlayer(), args[0], null);
}
if(args.length > 1) {
this.getAppliance().reportToKnown(this.getPlayer(), args[0], Arrays.stream(args).skip(1).collect(Collectors.joining(" ")));
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
List<String> response = new ArrayList<>();
if(args.length == 1) {
response = Stream.concat(
Bukkit.getOnlinePlayers().stream().map(Player::getName),
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
).toList();
}
if(args.length == 2) {
response = List.of(
"Griefing",
"Diebstahl",
"Beleidigung",
"Hacking",
"Andere Regelverstöße",
" "
);
}
return super.tabCompleteReducer(response, args);
}
}

View File

@ -0,0 +1,24 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class ReportsCommand extends ApplianceCommand.PlayerChecked<Report> {
public ReportsCommand() {
super("reports");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(ComponentUtil.pleaseWait());
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.getAppliance().queryReports(this.getPlayer())
);
}
}

View File

@ -0,0 +1,5 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
public interface CategorizedSetting {
SettingCategory category();
}

View File

@ -0,0 +1,7 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
public enum SettingCategory {
Gameplay,
Visuals,
Misc,
}

View File

@ -0,0 +1,198 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.Setting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners.OpenSettingsShortcutListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners.SettingsInventoryListener;
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class Settings extends Appliance {
private static Settings settingsInstance;
private final Set<Class<? extends Setting<?>>> declaredSettings = new HashSet<>();
public enum Key {
TechnicalTab,
ShowJoinAndLeaveMessages,
EnablePortableCrafting,
EnableSettingsShortcut,
AutoShulker,
SignEdit,
HotbarReplacer,
ChatMentions,
DoubleDoors,
KnockDoors,
BorderWarning
}
public static Settings instance() {
if(settingsInstance != null) return settingsInstance;
Settings.settingsInstance = Main.instance().getAppliance(Settings.class);
return settingsInstance;
}
public void declareSetting(Class<? extends Setting<?>> setting) {
this.declaredSettings.add(setting);
this.settingsCache.clear();
}
@Override
public void onEnable() {
Settings.instance().declareSetting(SettingsShortcutSetting.class);
}
public record OpenSettingsInventory(Inventory inventory, List<Setting<?>> settings) {
}
private final WeakHashMap<Player, OpenSettingsInventory> openSettingsInventories = new WeakHashMap<>();
private final WeakHashMap<Player, List<Setting<?>>> settingsCache = new WeakHashMap<>();
private List<Setting<?>> getSettings(Player player) {
if(this.settingsCache.containsKey(player)) return this.settingsCache.get(player);
List<Setting<?>> settings = this.declaredSettings.stream()
.map(clazz -> {
try {
return clazz.getDeclaredConstructor();
} catch(NoSuchMethodException e) {
throw new RuntimeException(String.format("Setting '%s' does not have an accessible constructor", clazz.getName()), e);
}
})
.map(constructor -> {
try {
return constructor.newInstance();
} catch(InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(String.format("Failed to create instance of setting '%s'", constructor.getDeclaringClass().getName()), e);
}
})
.peek(setting -> setting.initializeFromPlayer(player))
.collect(Collectors.toList());
this.settingsCache.put(player, settings);
return settings;
}
public <T> T getSetting(Player player, Key key, Class<T> clazz) {
Setting<?> setting = this.getSettings(player).stream()
.filter(s -> Objects.equals(s.getKey(), key))
.findFirst()
.orElseThrow();
if(!clazz.equals(setting.dataType()))
throw new IllegalStateException("Tried to retrieve Setting with Datatype " + clazz.getSimpleName() + " but expected " + setting.dataType().getSimpleName());
if(!clazz.isInstance(setting.state()))
throw new ClassCastException(clazz.getSimpleName() + " is not an instance of " + setting.dataType().getSimpleName());
return clazz.cast(setting.state());
}
public void openSettings(Player player) {
List<Setting<?>> settings = this.getSettings(player);
Inventory inventory = Bukkit.createInventory(null, this.calculateInvSize(settings), Component.text("Einstellungen"));
AtomicInteger row = new AtomicInteger(0);
Arrays.stream(SettingCategory.values())
.forEach(category -> {
List<Setting<?>> categorizedSettings = settings.stream()
.filter(setting -> setting instanceof CategorizedSetting)
.filter(setting -> ((CategorizedSetting) setting).category().equals(category))
.toList();
//skip empty category rows
if(categorizedSettings.isEmpty()) return;
for(int i = 0; i < categorizedSettings.size(); i++) {
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
if(i % 9 == 8) {
row.incrementAndGet();
}
}
row.incrementAndGet();
});
List<Setting<?>> uncategorizedSettings = settings.stream()
.filter(setting -> !(setting instanceof CategorizedSetting))
.toList();
for(int i = 0; i < uncategorizedSettings.size(); i++) {
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
if(i % 9 == 8) {
row.incrementAndGet();
}
}
player.openInventory(inventory);
InteractSounds.of(player).open();
this.openSettingsInventories.put(player, new OpenSettingsInventory(inventory, settings));
}
private int calculateInvSize(List<Setting<?>> settings) {
int countOfUncategorized = (int) settings.stream()
.filter(setting -> !(setting instanceof CategorizedSetting))
.count();
int invSizeForUncategorized = (int) Math.ceil((double) countOfUncategorized / 9) * 9;
int invSizeForCategorized = Arrays.stream(SettingCategory.values())
.map(settingCategory -> settings.stream()
.filter(setting -> setting instanceof CategorizedSetting)
.map(setting -> (CategorizedSetting) setting)
.filter(categorizedSetting -> categorizedSetting.category().equals(settingCategory))
.count())
.map(itemCount -> (int) Math.ceil((double) itemCount / 9))
.reduce(Integer::sum)
.orElse(1) * 9;
int invSize = invSizeForUncategorized + invSizeForCategorized;
if(invSize % 9 != 0) throw new IllegalStateException(
String.format("Failed to calculate settings inventory size. %d is not an multiple of 9", invSize)
);
return invSize;
}
public void onSettingsClose(Player player) {
if(!this.openSettingsInventories.containsKey(player)) return;
this.openSettingsInventories.remove(player);
player.updateInventory();
InteractSounds.of(player).close();
}
public boolean hasSettingsNotOpen(Player player) {
return !this.openSettingsInventories.containsKey(player);
}
public OpenSettingsInventory getOpenInventory(Player player) {
if(this.hasSettingsNotOpen(player))
throw new RuntimeException("Cannot retrieve data from closed Settings inventory!");
return this.openSettingsInventories.get(player);
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new SettingsInventoryListener(),
new OpenSettingsShortcutListener()
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new SettingsCommand());
}
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class SettingsCommand extends ApplianceCommand.PlayerChecked<Settings> {
public SettingsCommand() {
super("settings");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().openSettings(this.getPlayer());
}
}

View File

@ -0,0 +1,35 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class SettingsShortcutSetting extends BoolSetting implements CategorizedSetting {
public SettingsShortcutSetting() {
super(Settings.Key.EnableSettingsShortcut);
}
@Override
protected String title() {
return "Einstellungen mit 'Shift + F' öffnen";
}
@Override
protected String description() {
return "Wenn aktiviert öffnet sich dieses Einstellungsmenü durch das Drücken von 'Shift + F' im Spiel (Sneaken und Hand-Wechsel)";
}
@Override
protected Material icon() {
return Material.COMPARATOR;
}
@Override
protected Boolean defaultValue() {
return false;
}
@Override
public SettingCategory category() {
return SettingCategory.Misc;
}
}

View File

@ -0,0 +1,52 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
public abstract class ActionSetting extends Setting<Void> {
public ActionSetting() {
super(null);
}
protected abstract void onAction(Player player, ClickType clickType);
@Override
public ItemMeta buildMeta(ItemMeta meta) {
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
meta.lore(this.buildDescription(this.description()));
return meta;
}
@Override
protected void change(Player player, ClickType clickType) {
this.onAction(player, clickType);
}
@Override
protected Void defaultValue() {
return null;
}
@Override
protected void fromStorage(PersistentDataContainer container) {
}
@Override
protected void toStorage(PersistentDataContainer container, Void value) {
}
@Override
public Class<?> dataType() {
return null;
}
@Override
public Void state() {
return null;
}
}

View File

@ -0,0 +1,64 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class BoolSetting extends Setting<Boolean> {
public BoolSetting(Settings.Key key) {
super(key);
}
@Override
public void fromStorage(PersistentDataContainer container) {
this.state = container.has(this.getNamespacedKey())
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.BOOLEAN))
: this.defaultValue();
}
@Override
protected void toStorage(PersistentDataContainer container, Boolean value) {
container.set(this.getNamespacedKey(), PersistentDataType.BOOLEAN, value);
}
@Override
public ItemMeta buildMeta(ItemMeta meta) {
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>(List.of(
Component.empty()
.append(Component.text("Status: ", NamedTextColor.DARK_GRAY))
.append(Component.text(
this.state ? "Aktiviert" : "Deaktiviert",
this.state ? NamedTextColor.GREEN : NamedTextColor.RED)
),
Component.empty()
));
lore.addAll(this.buildDescription(this.description()));
meta.lore(lore);
return meta;
}
@Override
protected void change(Player player, ClickType clickType) {
this.state = !this.state;
}
@Override
public Class<?> dataType() {
return Boolean.class;
}
@Override
public Boolean state() {
return this.state;
}
}

View File

@ -0,0 +1,103 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public abstract class IntegerSetting extends Setting<Integer> {
private final List<Integer> options;
public IntegerSetting(Settings.Key key, int minimum, int maximum) {
this(key, IntStream.range(minimum, maximum+1).boxed().toList());
}
public IntegerSetting(Settings.Key key, List<Integer> options) {
super(key);
this.options = options;
}
@Override
public ItemMeta buildMeta(ItemMeta meta) {
Component componentBefore = Component.text(" " + this.fillWithSpaces(this.options.getLast()));
Component componentAfter = Component.text(" " + this.fillWithSpaces(this.options.getFirst()));
int listIndex = this.options.indexOf(this.state);
if(listIndex > 0) componentBefore = Component.text(" " + this.fillWithSpaces(this.options.get(listIndex-1)));
if(listIndex < this.options.size()-1) componentAfter = Component.text(" " + this.fillWithSpaces(this.options.get(listIndex+1)));
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>(Stream.of(
Component.empty()
.append(Component.text("Wert: ", NamedTextColor.DARK_GRAY)),
Component.empty()
.append(componentBefore.color(NamedTextColor.DARK_GRAY))
.append(Component.text(" " + this.fillWithSpaces(this.state), NamedTextColor.GREEN))
.append(componentAfter.color(NamedTextColor.DARK_GRAY)),
Component.empty()
).toList());
lore.addAll(this.buildDescription(this.description()));
meta.lore(lore);
return meta;
}
private String fillWithSpaces(Integer option) {
String optionString = option.toString();
int optionLength = optionString.length();
int maxInteger = this.options.stream().mapToInt(value -> value).max().orElse(0);
int maxLength = String.valueOf(maxInteger).length();
int padding = maxLength - optionLength;
int padEnd = padding / 2;
int padStart = padding - padEnd;
optionString = " ".repeat(padStart) + optionString + " ".repeat(padEnd);
return optionString;
}
@Override
protected void change(Player player, ClickType clickType) {
int elementBefore = this.options.getLast();
int elementAfter = this.options.getFirst();
int listIndex = this.options.indexOf(this.state);
if(listIndex > 0) elementBefore = this.options.get(listIndex-1);
if(listIndex < this.options.size()-1) elementAfter = this.options.get(listIndex+1);
if(clickType.equals(ClickType.LEFT)) this.state = elementBefore;
if(clickType.equals(ClickType.RIGHT)) this.state = elementAfter;
}
@Override
protected void fromStorage(PersistentDataContainer container) {
this.state = container.has(this.getNamespacedKey())
? Integer.valueOf(Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING)))
: this.defaultValue();
if(!this.options.contains(this.state)) this.state = this.defaultValue();
}
@Override
protected void toStorage(PersistentDataContainer container, Integer value) {
container.set(this.getNamespacedKey(), PersistentDataType.STRING, new Gson().toJson(value));
}
@Override
public Class<?> dataType() {
return Integer.class;
}
@Override
public Integer state() {
return this.state;
}
}

View File

@ -0,0 +1,153 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
public abstract class MultiBoolSetting<T> extends Setting<T> {
private String cursorPosition;
public MultiBoolSetting(Settings.Key key) {
super(key);
}
@Retention(RetentionPolicy.RUNTIME)
public @interface DisplayName {
String value();
}
@Override
public ItemMeta buildMeta(ItemMeta meta) {
record SettingField(String name, String displayName, Boolean value) {
}
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>();
lore.add(Component.text("Status: ", NamedTextColor.DARK_GRAY));
lore.addAll(
Arrays.stream(this.state.getClass().getRecordComponents())
.map(component -> {
try {
Method method = this.state.getClass().getDeclaredMethod(component.getName());
Boolean value = (Boolean) method.invoke(this.state);
DisplayName annotation = component.getAnnotation(DisplayName.class);
String displayName = annotation != null
? annotation.value()
: component.getName();
return new SettingField(component.getName(), displayName, value);
} catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.map(field -> {
if(this.cursorPosition == null) this.cursorPosition = field.name;
boolean isSelected = field.name.equals(this.cursorPosition);
return Component.text()
.append(Component.text(
isSelected ? "> " : " ",
isSelected ? NamedTextColor.GREEN : NamedTextColor.GRAY)
)
.append(Component.text(
field.displayName + ": ",
isSelected ? NamedTextColor.DARK_GREEN : NamedTextColor.GRAY)
)
.append(Component.text(
field.value ? "Aktiviert" : "Deaktiviert",
field.value ? NamedTextColor.GREEN : NamedTextColor.RED)
)
.build();
})
.toList()
);
lore.add(Component.empty());
lore.addAll(this.buildDescription(this.description()));
lore.add(Component.empty());
lore.add(Component.text("Linksklick", NamedTextColor.AQUA).append(Component.text(" zum Wählen der Option", NamedTextColor.GRAY)));
lore.add(Component.text("Rechtsklick", NamedTextColor.AQUA).append(Component.text(" zum Ändern des Wertes", NamedTextColor.GRAY)));
meta.lore(lore);
return meta;
}
@Override
protected void change(Player player, ClickType clickType) {
var recordComponents = this.state.getClass().getRecordComponents();
int currentIndex = IntStream.range(0, recordComponents.length)
.filter(i -> recordComponents[i].getName().equals(this.cursorPosition))
.findFirst()
.orElse(-1);
if(clickType.equals(ClickType.LEFT)) {
currentIndex = (currentIndex + 1) % recordComponents.length;
this.cursorPosition = recordComponents[currentIndex].getName();
} else if(clickType.equals(ClickType.RIGHT)) {
try {
Object[] values = Arrays.stream(recordComponents)
.map(rc -> {
try {
return this.state.getClass().getDeclaredMethod(rc.getName()).invoke(this.state);
} catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
})
.toArray();
Method getter = this.state.getClass().getDeclaredMethod(this.cursorPosition);
boolean currentValue = (Boolean) getter.invoke(this.state);
values[currentIndex] = !currentValue;
//noinspection unchecked
this.state = (T) this.state.getClass().getConstructor(
Arrays.stream(recordComponents).map(RecordComponent::getType).toArray(Class[]::new)
).newInstance(values);
} catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
throw new RuntimeException(e);
}
}
}
@Override
protected void fromStorage(PersistentDataContainer container) {
String data = container.has(this.getNamespacedKey())
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING))
: new Gson().toJson(this.defaultValue());
try {
//noinspection unchecked
this.state = (T) new Gson().fromJson(data, this.dataType());
} catch(Exception e) {
this.state = this.defaultValue();
}
}
@Override
protected void toStorage(PersistentDataContainer container, T value) {
container.set(this.getNamespacedKey(), PersistentDataType.STRING, new Gson().toJson(value));
}
@Override
public T state() {
return this.state;
}
}

View File

@ -0,0 +1,106 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
public abstract class SelectSetting extends Setting<SelectSetting.Options.Option> {
private final Options options;
public SelectSetting(Settings.Key key, Options options) {
super(key);
this.options = options;
}
public record Options(List<Option> options) {
public record Option(String name, NamespacedKey key) {
public boolean is(Option other) {
return this.equals(other);
}
}
}
protected abstract Material icon();
protected abstract Options.Option defaultValue();
@Override
public ItemMeta buildMeta(ItemMeta meta) {
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>();
lore.add(Component.text("Status: ", NamedTextColor.DARK_GRAY));
lore.addAll(
this.options.options.stream()
.map(option -> {
boolean isSelected = option.equals(this.state);
return Component.text()
.append(Component.text(isSelected ? "> " : " ", isSelected ? NamedTextColor.GREEN : NamedTextColor.GRAY))
.append(Component.text(option.name, isSelected ? NamedTextColor.DARK_GREEN : NamedTextColor.DARK_GRAY))
.build();
})
.toList()
);
lore.add(Component.empty());
lore.addAll(this.buildDescription(this.description()));
meta.lore(lore);
return meta;
}
@Override
protected void change(Player player, ClickType clickType) {
int optionModifier = clickType.equals(ClickType.LEFT) ? 1 : -1;
List<Options.Option> options = this.options.options;
this.state = IntStream.range(0, options.size())
.filter(i -> options.get(i).equals(this.state))
.mapToObj(i -> {
int nextIndex = i + optionModifier;
if(nextIndex >= options.size()) {
return options.getFirst();
} else if(nextIndex < 0) {
return options.getLast();
} else {
return options.get(nextIndex);
}
})
.findFirst()
.orElseThrow();
}
@Override
protected void fromStorage(PersistentDataContainer container) {
String data = container.has(this.getNamespacedKey())
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING))
: this.defaultValue().key.asString();
this.state = this.options.options.stream()
.filter(option -> option.key.asString().equals(data))
.findFirst()
.orElse(this.defaultValue());
}
@Override
protected void toStorage(PersistentDataContainer container, Options.Option value) {
container.set(this.getNamespacedKey(), PersistentDataType.STRING, value.key.asString());
}
@Override
public Class<?> dataType() {
return Options.Option.class;
}
@Override
public Options.Option state() {
return this.state;
}
}

View File

@ -0,0 +1,69 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import java.util.List;
public abstract class Setting<TDataType> {
protected TDataType state;
private final Settings.Key key;
public Setting(Settings.Key key) {
this.key = key;
}
public NamespacedKey getNamespacedKey() {
return new NamespacedKey(Main.instance(), this.key.name());
}
public Settings.Key getKey() {
return this.key;
}
public void initializeFromPlayer(Player p) {
this.fromStorage(p.getPersistentDataContainer());
}
public void triggerChange(Player p, ClickType clickType) {
if(clickType.equals(ClickType.DOUBLE_CLICK)) return;
this.change(p, clickType);
InteractSounds.of(p).click();
this.toStorage(p.getPersistentDataContainer(), this.state());
}
public ItemStack buildItem() {
ItemStack stack = new ItemStack(this.icon(), 1);
stack.setItemMeta(this.buildMeta(stack.getItemMeta()));
return stack;
}
protected List<TextComponent> buildDescription(String description) {
return ComponentUtil.lineBreak(description, 50)
.map(s -> Component.text(s, NamedTextColor.GRAY))
.toList();
}
protected abstract String title();
protected abstract String description();
protected abstract Material icon();
public abstract ItemMeta buildMeta(ItemMeta meta);
protected abstract void change(Player player, ClickType clickType);
protected abstract TDataType defaultValue();
protected abstract void fromStorage(PersistentDataContainer container);
protected abstract void toStorage(PersistentDataContainer container, TDataType value);
public abstract Class<?> dataType();
public abstract TDataType state();
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
public class OpenSettingsShortcutListener extends ApplianceListener<Settings> {
@EventHandler
public void onItemSwitch(PlayerSwapHandItemsEvent event) {
if(!event.getPlayer().isSneaking()) return;
if(!Settings.instance().getSetting(event.getPlayer(), Settings.Key.EnableSettingsShortcut, Boolean.class))
return;
event.setCancelled(true);
this.getAppliance().openSettings(event.getPlayer());
}
}

View File

@ -0,0 +1,31 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
public class SettingsInventoryListener extends ApplianceListener<Settings> {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
Player player = (Player) event.getWhoClicked();
if(this.getAppliance().hasSettingsNotOpen(player)) return;
event.setCancelled(true);
Settings.OpenSettingsInventory openInventory = this.getAppliance().getOpenInventory(player);
openInventory.settings().stream()
.filter(setting -> setting.buildItem().equals(event.getCurrentItem()))
.findFirst()
.ifPresent(setting -> {
setting.triggerChange(player, event.getClick());
openInventory.inventory().setItem(event.getSlot(), setting.buildItem());
});
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
this.getAppliance().onSettingsClose((Player) event.getPlayer());
}
}

View File

@ -0,0 +1,91 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.util.IteratorUtil;
import eu.mhsl.craftattack.spawn.core.util.statistics.NetworkMonitor;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.spawn.core.util.text.RainbowComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.time.Duration;
import java.util.List;
public class Tablist extends Appliance {
private final String projectTitle = this.localConfig().getString("projectTitle", "Title not configured");
private final RainbowComponent serverName = new RainbowComponent(String.format(" %s ", this.projectTitle), 7, 3);
private NetworkMonitor networkMonitor;
private OperatingSystemMXBean systemMonitor;
public Tablist() {
super("tablist");
}
@Override
public void onEnable() {
Settings.instance().declareSetting(TechnicalTablistSetting.class);
int tabRefreshRate = 3;
this.networkMonitor = new NetworkMonitor(this.localConfig().getString("interface"), Duration.ofSeconds(1));
this.systemMonitor = ManagementFactory.getOperatingSystemMXBean();
Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
() -> IteratorUtil.onlinePlayers(this::updateHeader),
tabRefreshRate * Ticks.TICKS_PER_SECOND,
tabRefreshRate * Ticks.TICKS_PER_SECOND
);
}
@Override
public void onDisable() {
this.networkMonitor.stop();
}
public void fullUpdate(Player player) {
this.updateHeader(player);
this.updateFooter(player);
}
private void updateHeader(Player player) {
boolean detailedInfo = this.queryAppliance(Settings.class).getSetting(player, Settings.Key.TechnicalTab, Boolean.class);
Component header = Component.newline()
.append(this.serverName.getRainbowState()).appendNewline()
.append(Component.text("mhsl.eu", NamedTextColor.GOLD)).appendNewline().appendNewline()
.append(ComponentUtil.getFormattedTickTimes(detailedInfo)).appendNewline();
if(detailedInfo) {
header = header
.appendNewline()
.append(ComponentUtil.getFormattedPing(player)).appendNewline()
.append(ComponentUtil.getFormattedNetworkStats(
this.networkMonitor.getTraffic(),
this.networkMonitor.getPackets()
)).appendNewline()
.append(ComponentUtil.getFormattedSystemStats(this.systemMonitor)).appendNewline();
}
player.sendPlayerListHeader(header);
}
private void updateFooter(Player player) {
player.sendPlayerListFooter(Report.helpText());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new TablistListener());
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class TablistListener extends ApplianceListener<Tablist> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
this.getAppliance().fullUpdate(event.getPlayer());
}
}

View File

@ -0,0 +1,38 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class TechnicalTablistSetting extends BoolSetting implements CategorizedSetting {
public TechnicalTablistSetting() {
super(Settings.Key.TechnicalTab);
}
@Override
protected String title() {
return "Technische Informationen";
}
@Override
protected String description() {
return "Zeige erweiterte Informationen und Statistiken in der Tabliste an";
}
@Override
protected Material icon() {
return Material.COMMAND_BLOCK_MINECART;
}
@Override
protected Boolean defaultValue() {
return false;
}
@Override
public SettingCategory category() {
return SettingCategory.Misc;
}
}

View File

@ -0,0 +1,128 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class AcInform extends Appliance {
public void processCommand(@NotNull String[] args) {
String anticheatName = null;
String playerName = null;
String checkName = null;
Float violationCount = null;
for(int i = 0; i < args.length; i++) {
if(!args[i].startsWith("--")) continue;
StringBuilder valueBuilder = new StringBuilder();
for(int j = i + 1; j < args.length; j++) {
if(args[j].startsWith("--")) break;
if(!valueBuilder.isEmpty()) valueBuilder.append(" ");
valueBuilder.append(args[j]);
}
String value = valueBuilder.toString();
switch(args[i]) {
case "--anticheatName" -> anticheatName = value;
case "--playerName" -> playerName = value;
case "--check" -> checkName = value;
case "--violationCount" -> violationCount = value.isEmpty() ? null : Float.valueOf(value);
}
}
this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
}
public void notifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float violationCount) {
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text();
NamedTextColor textColor = NamedTextColor.GRAY;
if(playerName == null || playerName.isBlank())
throw new ApplianceCommand.Error("acinform command needs a player (--playerName)");
if(anticheatName != null && !anticheatName.isBlank()) {
component
.append(Component.text(" ", NamedTextColor.GRAY))
.append(Component.text("[", textColor))
.append(Component.text(anticheatName, NamedTextColor.RED))
.append(Component.text("]: ", textColor));
}
component
.append(Component.text("Player ", textColor))
.append(Component.text(playerName, NamedTextColor.WHITE))
.append(Component.text(" "));
if(checkName == null || checkName.isBlank()) {
component.append(Component.text("got detected by Anticheat", textColor));
} else if(violationCount != null) {
component.append(
Component.text("failed ", textColor)
.append(Component.text(String.format("%sx ", violationCount), NamedTextColor.WHITE))
.append(Component.text("'", textColor))
.append(Component.text(checkName, NamedTextColor.WHITE))
.append(Component.text("'", textColor))
);
} else {
component.append(
Component.text("failed ", textColor)
.append(Component.text("'", textColor))
.append(Component.text(checkName, NamedTextColor.WHITE))
.append(Component.text("'", textColor))
);
}
component.append(
Component.newline()
.append(Component.text("", NamedTextColor.GRAY))
.append(Component.text("[", NamedTextColor.GRAY))
.append(Component.text("Report", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/report %s anticheat %s flagged %s", playerName, anticheatName, checkName)))
);
component.append(
Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("Kick", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/kick %s", playerName)))
);
component.append(
Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("Panic Ban", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/panicban %s", playerName)))
);
component.append(
Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("Spectate/Teleport", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/grim spectate %s", playerName)))
);
TextComponent finalMessage = component.build();
Bukkit.getOnlinePlayers().stream()
.filter(player -> player.hasPermission("admin"))
.forEach(player -> player.sendMessage(finalMessage));
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new AcInformCommand()
);
}
}

View File

@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
class AcInformCommand extends ApplianceCommand<AcInform> {
public AcInformCommand() {
super("acInform");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(sender instanceof Player) throw new ApplianceCommand.Error("Dieser Command ist nicht für Spieler!");
this.getAppliance().processCommand(args);
}
}

View File

@ -0,0 +1,37 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.adminChat;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AdminChat extends Appliance {
public void sendMessage(Player sender, String message) {
Component privatePrefix = Component
.text("[Admin] ", NamedTextColor.LIGHT_PURPLE)
.clickEvent(ClickEvent.suggestCommand(AdminChatCommand.commandName));
Bukkit.getOnlinePlayers().stream()
.filter(player -> player.hasPermission("admin"))
.forEach(player -> {
Component formattedMessage = Component.text()
.append(privatePrefix)
.append(sender.displayName())
.append(Component.text(" > ", NamedTextColor.DARK_GRAY))
.append(Component.text(message))
.build();
player.sendMessage(formattedMessage);
});
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new AdminChatCommand());
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.adminChat;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class AdminChatCommand extends ApplianceCommand.PlayerChecked<AdminChat> {
public static final String commandName = "adminchat";
public AdminChatCommand() {
super(commandName);
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(!sender.hasPermission("admin")) return;
String message = String.join(" ", args);
this.getAppliance().sendMessage(this.getPlayer(), message);
}
}

View File

@ -0,0 +1,44 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.chatMute;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public class ChatMute extends Appliance {
private static final String namespace = ChatMute.class.getSimpleName().toLowerCase(Locale.ROOT);
public static NamespacedKey mutedUntilKey = new NamespacedKey(namespace, "mutedUntilMillis".toLowerCase());
public void mutePlayer(Player player, int durationHours) {
PersistentDataContainer container = player.getPersistentDataContainer();
long mutedUntil = System.currentTimeMillis() + (long) durationHours * 60 * 60 * 1000;
container.set(ChatMute.mutedUntilKey, PersistentDataType.LONG, mutedUntil);
}
public @Nullable Long muteStatus(Player player) {
PersistentDataContainer container = player.getPersistentDataContainer();
if(!container.has(mutedUntilKey)) return null;
long mutedUntil = Objects.requireNonNull(container.get(mutedUntilKey, PersistentDataType.LONG));
if(mutedUntil < System.currentTimeMillis()) return null;
return mutedUntil;
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new MuteCommand());
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new ChatMuteListener());
}
}

View File

@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.chatMute;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.util.text.DataSizeConverter;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.event.EventHandler;
import org.jetbrains.annotations.Nullable;
class ChatMuteListener extends ApplianceListener<ChatMute> {
@EventHandler
public void onChat(AsyncChatEvent event) {
@Nullable Long muteDuration = this.getAppliance().muteStatus(event.getPlayer());
if(muteDuration == null) return;
event.setCancelled(true);
event.getPlayer().sendMessage(Component.text(
String.format(
"Du bist für %s gestummt!",
DataSizeConverter.formatSecondsToHumanReadable((int) ((muteDuration - System.currentTimeMillis()) / 1000))
),
NamedTextColor.RED
));
}
}

View File

@ -0,0 +1,35 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.chatMute;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
class MuteCommand extends ApplianceCommand<ChatMute> {
public MuteCommand() {
super("mute");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length < 2) throw new Error("Syntax: mute <player> <duration in hours>");
Player player = Objects.requireNonNull(Bukkit.getPlayer(args[0]));
int durationInHours = Integer.parseInt(args[1]);
this.getAppliance().mutePlayer(player, durationInHours);
sender.sendMessage(String.format("%s wurde für %d Stunden gestummt!", player.getName(), durationInHours));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 2) {
return List.of("1", "2", "4", "8", "24", "48");
}
return null;
}
}

View File

@ -0,0 +1,63 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class EndPrevent extends Appliance {
private final String endPreventKey = "endPrevent";
private State endPreventState;
private final World endWorld = Bukkit.getWorlds().stream().filter(world -> world.getEnvironment().equals(World.Environment.THE_END)).findFirst().orElseThrow();
public enum State {
OPEN,
CLOSED,
NO_OUTER
}
public EndPrevent() {
super("endPrevent");
this.endPreventState = State.valueOf(this.localConfig().getString(this.endPreventKey, State.OPEN.name()));
this.updateEndBorder();
}
private void updateEndBorder() {
if(this.endPreventState == State.NO_OUTER) this.endWorld.getWorldBorder().setSize(500);
if(this.endPreventState == State.OPEN) this.endWorld.getWorldBorder().setSize(this.endWorld.getWorldBorder().getMaxSize());
}
public void setEndState(State state) {
this.localConfig().set(this.endPreventKey, state.name());
Configuration.saveChanges();
this.endPreventState = state;
this.updateEndBorder();
}
public boolean isEndClosed() {
return this.endPreventState.equals(State.CLOSED);
}
public boolean isOnlyInner() {
return this.endPreventState.equals(State.NO_OUTER);
}
public State getEndPreventState() {
return this.endPreventState;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new EndPreventListener());
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new EndPreventCommand());
}
}

View File

@ -0,0 +1,41 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
class EndPreventCommand extends ApplianceCommand<EndPrevent> {
private final Map<String, EndPrevent.State> arguments = Map.of(
"preventEnd", EndPrevent.State.CLOSED,
"allowEnd", EndPrevent.State.OPEN,
"onlyInnerEnd", EndPrevent.State.NO_OUTER
);
public EndPreventCommand() {
super("endPrevent");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length == 1 && this.arguments.containsKey(args[0])) {
this.getAppliance().setEndState(this.arguments.get(args[0]));
sender.sendMessage(Component.text("Setting updated!", NamedTextColor.GREEN));
}
sender.sendMessage(Component.text(
String.format("The End is now on '%s'!", this.getAppliance().getEndPreventState().name()),
NamedTextColor.GOLD
));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return this.arguments.keySet().stream().toList();
}
}

View File

@ -0,0 +1,49 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
import com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerPortalEvent;
class EndPreventListener extends ApplianceListener<EndPrevent> {
@EventHandler
public void onEnderEyeInteraction(PlayerInteractEvent event) {
if(event.getClickedBlock() == null) return;
if(!event.getClickedBlock().getType().equals(Material.END_PORTAL_FRAME)) return;
if(event.getItem() == null) return;
if(!event.getItem().getType().equals(Material.ENDER_EYE)) return;
this.cancelIfClosed(event, event.getPlayer());
}
@EventHandler
public void onPlayerPortal(PlayerPortalEvent event) {
if(!event.getTo().getWorld().getEnvironment().equals(World.Environment.THE_END)) return;
this.cancelIfClosed(event, event.getPlayer());
}
@EventHandler
public void onPlayerEndGateway(PlayerTeleportEndGatewayEvent event) {
this.cancelIfOnlyInner(event, event.getPlayer());
}
private void cancelIfClosed(Cancellable event, Player p) {
if(!this.getAppliance().isEndClosed()) return;
event.setCancelled(true);
p.sendActionBar(Component.text("Das End ist nicht freigeschaltet!", NamedTextColor.RED));
}
private void cancelIfOnlyInner(Cancellable event, Player p) {
if(!this.getAppliance().isOnlyInner()) return;
event.setCancelled(true);
p.sendActionBar(Component.text("Das Outer End ist nicht freigeschaltet!", NamedTextColor.RED));
}
}

View File

@ -0,0 +1,33 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.kick;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class Kick extends Appliance {
public void kick(@NotNull String playerName, @Nullable String message) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
new DisconnectInfo(
"Administrator Eingriff",
"Du wurdest von einem Admin vom Server geworfen.",
message,
player.getUniqueId()
).applyKick(player);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new KickCommand());
}
}

View File

@ -0,0 +1,35 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.kick;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class KickCommand extends ApplianceCommand<Kick> {
public KickCommand() {
super("kick");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().kick(
args[0],
Arrays.stream(args).skip(1).collect(Collectors.joining(" "))
);
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return super.tabCompleteReducer(
Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(),
args
);
}
}

View File

@ -0,0 +1,43 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.maintenance;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Maintenance extends Appliance {
private boolean isInMaintenance;
private final String configKey = "enabled";
public Maintenance() {
super("maintenance");
}
@Override
public void onEnable() {
this.isInMaintenance = this.localConfig().getBoolean(this.configKey, false);
}
public void setState(boolean enabled) {
this.isInMaintenance = enabled;
this.localConfig().set(this.configKey, enabled);
Configuration.saveChanges();
}
public boolean isInMaintenance() {
return this.isInMaintenance;
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new MaintenanceCommand());
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new PreventMaintenanceJoinListener());
}
}

View File

@ -0,0 +1,37 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.maintenance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
class MaintenanceCommand extends ApplianceCommand<Maintenance> {
private final Map<String, Boolean> arguments = Map.of("enable", true, "disable", false);
public MaintenanceCommand() {
super("maintanance");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length == 1 && this.arguments.containsKey(args[0])) {
this.getAppliance().setState(this.arguments.get(args[0]));
sender.sendMessage(Component.text("Maintanance mode updated!", NamedTextColor.GREEN));
}
sender.sendMessage(Component.text(
String.format("Maintanance mode is %b", this.getAppliance().isInMaintenance()),
NamedTextColor.GOLD
));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return this.arguments.keySet().stream().toList();
}
}

View File

@ -0,0 +1,23 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.maintenance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerLoginEvent;
class PreventMaintenanceJoinListener extends ApplianceListener<Maintenance> {
@EventHandler
public void onJoin(PlayerLoginEvent event) {
if(!this.getAppliance().isInMaintenance()) return;
if(event.getPlayer().hasPermission("bypassMaintainance")) return;
DisconnectInfo disconnectInfo = new DisconnectInfo(
"Server für Spieler nicht verfügbar",
"Zurzeit können nur Admins dem Server beitreten!",
"Bitte versuche es später erneut.",
event.getPlayer().getUniqueId()
);
event.disallow(PlayerLoginEvent.Result.KICK_OTHER, disconnectInfo.getComponent());
}
}

View File

@ -0,0 +1,56 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.panicBan;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class PanicBan extends Appliance {
private final Map<UUID, Long> panicBans = new HashMap<>();
public void panicBan(@NotNull String playerName) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
this.panicBans.put(player.getUniqueId(), System.currentTimeMillis());
this.getDisconnectInfo(player.getUniqueId()).applyKick(player);
}
public boolean isBanned(UUID player) {
if(this.panicBans.containsKey(player) && this.panicBans.get(player) < System.currentTimeMillis() - 15 * 60 * 1000)
this.panicBans.remove(player);
return this.panicBans.containsKey(player);
}
public DisconnectInfo getDisconnectInfo(UUID playerUuid) {
return new DisconnectInfo(
"Temporäre Sperre",
"Du wurdest von einen Admin vom Server geworfen!",
"Du bist bis zur endgültigen Klärung gesperrt.",
playerUuid
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new PanicBanCommand());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new PanicBanJoinListener());
}
}

View File

@ -0,0 +1,36 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.panicBan;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
class PanicBanCommand extends ApplianceCommand<PanicBan> {
public PanicBanCommand() {
super("panicBan");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().panicBan(args[0]);
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return super.tabCompleteReducer(
Stream.concat(
Bukkit.getOnlinePlayers().stream().map(Player::getName),
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
).toList(),
args
);
}
}

View File

@ -0,0 +1,15 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.panicBan;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
class PanicBanJoinListener extends ApplianceListener<PanicBan> {
@EventHandler
public void onLogin(AsyncPlayerPreLoginEvent event) {
if(this.getAppliance().isBanned(event.getUniqueId())) {
event.kickMessage(this.getAppliance().getDisconnectInfo(event.getUniqueId()).getComponent());
event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_BANNED);
}
}
}

View File

@ -0,0 +1,47 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class PlayerLimit extends Appliance {
private static final String playerLimitKey = "maxPlayers";
private int limit;
public PlayerLimit() {
super("playerLimit");
this.limit = this.localConfig().getInt(playerLimitKey);
Bukkit.setMaxPlayers(Integer.MAX_VALUE);
}
public void setPlayerLimit(int limit) {
this.limit = limit;
this.localConfig().set(playerLimitKey, limit);
Configuration.saveChanges();
}
public int getLimit() {
return this.limit;
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new PlayerLimiterListener()
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new SetPlayerLimitCommand()
);
}
}

View File

@ -0,0 +1,24 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
class PlayerLimiterListener extends ApplianceListener<PlayerLimit> {
@EventHandler
public void onLogin(AsyncPlayerPreLoginEvent playerPreLoginEvent) {
playerPreLoginEvent.kickMessage(
new DisconnectInfo(
"Hohe Serverauslastung",
"Der Server ist momentan an seiner Kapazitätsgrenze angelangt!",
"Bitte versuche es zu einem späteren Zeitpunkt erneut.",
playerPreLoginEvent.getUniqueId()
).getComponent()
);
if(Bukkit.getOnlinePlayers().size() >= this.getAppliance().getLimit())
playerPreLoginEvent.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_FULL);
}
}

View File

@ -0,0 +1,28 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class SetPlayerLimitCommand extends ApplianceCommand<PlayerLimit> {
public SetPlayerLimitCommand() {
super("setPlayerLimit");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 0) {
sender.sendMessage(
Component.text()
.append(Component.text("Das aktuelle Spielerlimit beträgt: ", NamedTextColor.GRAY))
.append(Component.text(this.getAppliance().getLimit(), NamedTextColor.GOLD))
);
return;
}
this.getAppliance().setPlayerLimit(Integer.parseInt(args[0]));
}
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.restart;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class CancelRestartCommand extends ApplianceCommand<Restart> {
public CancelRestartCommand() {
super("cancelRestart");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().cancelRestart();
}
}

View File

@ -0,0 +1,59 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.restart;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.IteratorUtil;
import eu.mhsl.craftattack.spawn.core.util.text.Countdown;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Restart extends Appliance {
private final Countdown countdown;
public Restart() {
this.countdown = new Countdown(60, this::format, this::announce, this::onDone);
}
public void scheduleRestart() {
try {
this.countdown.start();
} catch(IllegalStateException e) {
throw new ApplianceCommand.Error(e.getMessage());
}
}
public void cancelRestart() {
try {
this.countdown.cancel();
this.announce(Component.text("Der geplante Serverneustart wurde abgebrochen!", NamedTextColor.RED));
} catch(IllegalStateException e) {
throw new ApplianceCommand.Error(e.getMessage());
}
}
private Component format(Countdown.AnnouncementData data) {
return Component.text()
.append(Component.text("Serverneustart in ", NamedTextColor.DARK_RED))
.append(Component.text(data.count(), NamedTextColor.RED))
.append(Component.text(" " + data.unit() + "!", NamedTextColor.DARK_RED))
.build();
}
private void announce(Component message) {
IteratorUtil.onlinePlayers(player -> player.sendMessage(message));
}
private void onDone() {
Bukkit.shutdown();
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new ScheduleRestartCommand(), new CancelRestartCommand());
}
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.restart;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class ScheduleRestartCommand extends ApplianceCommand<Restart> {
public ScheduleRestartCommand() {
super("scheduleRestart");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().scheduleRestart();
}
}

7
core/build.gradle Normal file
View File

@ -0,0 +1,7 @@
dependencies {
compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT'
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4'
implementation 'org.reflections:reflections:0.10.2'
}

View File

@ -0,0 +1,126 @@
package eu.mhsl.craftattack.spawn.core;
import eu.mhsl.craftattack.spawn.core.api.client.RepositoryLoader;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin;
import org.reflections.Reflections;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class Main extends JavaPlugin {
public static final String projectPackage = "eu.mhsl.craftattack.spawn";
private static Main instance;
private static Logger logger;
private List<Appliance> appliances;
private RepositoryLoader repositoryLoader;
private Reflections reflections;
@Override
public void onEnable() {
instance = this;
logger = instance().getLogger();
this.saveDefaultConfig();
try {
this.wrappedEnable();
} catch(Exception e) {
Main.logger().log(Level.SEVERE, "Error while initializing Spawn plugin, shutting down!", e);
Bukkit.shutdown();
}
}
private void wrappedEnable() {
Configuration.readConfig();
List<String> disabledAppliances = Configuration.pluginConfig.getStringList("disabledAppliances");
Main.logger().info("Initializing reflections...");
this.reflections = new Reflections(projectPackage);
Main.logger().info("Loading Repositories...");
this.repositoryLoader = new RepositoryLoader();
Main.logger().info(String.format("Loaded %d repositories!", this.repositoryLoader.getRepositories().size()));
Main.logger().info("Loading appliances...");
this.appliances = this.findSubtypesOf(Appliance.class).stream()
.filter(applianceClass -> !disabledAppliances.contains(applianceClass.getSimpleName()))
.map(applianceClass -> {
try {
return (Appliance) applianceClass.getDeclaredConstructor().newInstance();
} catch(Exception e) {
throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e);
}
})
.toList();
Main.logger().info(String.format("Loaded %d appliances!", this.appliances.size()));
Main.logger().info("Initializing appliances...");
this.appliances.forEach(appliance -> {
appliance.onEnable();
appliance.initialize(this);
});
Main.logger().info(String.format("Initialized %d appliances!", this.appliances.size()));
if(Configuration.pluginConfig.getBoolean("httpServerEnabled", true)) {
Main.logger().info("Starting HTTP API...");
new HttpServer();
}
this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
Main.logger().info("Startup complete!");
}
@Override
public void onDisable() {
Main.logger().info("Disabling appliances...");
this.appliances.forEach(appliance -> {
Main.logger().info("Disabling " + appliance.getClass().getSimpleName());
appliance.onDisable();
appliance.destruct(this);
});
HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTasks(this);
Main.logger().info("Disabled " + this.appliances.size() + " appliances!");
}
public <T extends Appliance> T getAppliance(Class<T> clazz) {
return this.appliances.stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.findFirst()
.orElseThrow(() -> new RuntimeException(String.format("Appliance %s not loaded or instantiated!", clazz)));
}
public <T> Set<Class<? extends T>> findSubtypesOf(Class<T> type) {
return this.reflections.getSubTypesOf(type);
}
@SuppressWarnings("unchecked")
public static <T> Class<T> getApplianceType(Class<?> clazz) {
return (Class<T>) ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[0];
}
public List<Appliance> getAppliances() {
return this.appliances;
}
public RepositoryLoader getRepositoryLoader() {
return this.repositoryLoader;
}
public static Main instance() {
return instance;
}
public static Logger logger() {
return logger;
}
}

View File

@ -0,0 +1,14 @@
package eu.mhsl.craftattack.spawn.core.api;
public class HttpStatus {
public static final int OK = 200;
public static final int CREATED = 201;
public static final int ACCEPTED = 202;
public static final int NO_CONTENT = 204;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int INTERNAL_SERVER_ERROR = 500;
public static final int SERVICE_UNAVAILABLE = 503;
}

View File

@ -0,0 +1,127 @@
package eu.mhsl.craftattack.spawn.core.api.client;
import eu.mhsl.craftattack.spawn.core.Main;
import org.apache.http.client.utils.URIBuilder;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@RepositoryLoader.Abstraction
public abstract class HttpRepository extends Repository {
public record RequestModifier(
@Nullable Consumer<URIBuilder> uri,
@Nullable Consumer<HttpRequest.Builder> header
) {}
private final List<RequestModifier> baseRequestModifier;
public HttpRepository(URI basePath) {
this(basePath, new RequestModifier(null, null));
}
public HttpRepository(URI basePath, RequestModifier... baseRequestModifier) {
super(basePath);
this.baseRequestModifier = baseRequestModifier == null
? List.of()
: List.of(baseRequestModifier);
}
protected <TInput, TOutput> ReqResp<TOutput> post(String command, TInput data, Class<TOutput> outputType) {
return this.post(command, parameters -> {
}, data, outputType);
}
protected <TInput, TOutput> ReqResp<TOutput> post(String command, Consumer<URIBuilder> parameters, TInput data, Class<TOutput> outputType) {
HttpRequest request = this.getRequestBuilder(this.getUri(command, parameters))
.POST(HttpRequest.BodyPublishers.ofString(this.gson.toJson(data)))
.build();
return this.execute(request, outputType, data);
}
protected <TInput, TOutput> ReqResp<TOutput> put(String command, TInput data, Class<TOutput> outputType) {
return this.put(command, parameters -> {
}, data, outputType);
}
protected <TInput, TOutput> ReqResp<TOutput> put(String command, Consumer<URIBuilder> parameters, TInput data, Class<TOutput> outputType) {
HttpRequest request = this.getRequestBuilder(this.getUri(command, parameters))
.PUT(HttpRequest.BodyPublishers.ofString(this.gson.toJson(data)))
.build();
return this.execute(request, outputType, data);
}
protected <TOutput> ReqResp<TOutput> get(String command, Class<TOutput> outputType) {
return this.get(command, parameters -> {
}, outputType);
}
protected <TOutput> ReqResp<TOutput> get(String command, Consumer<URIBuilder> parameters, Class<TOutput> outputType) {
HttpRequest request = this.getRequestBuilder(this.getUri(command, parameters))
.GET()
.build();
return this.execute(request, outputType, null);
}
private URI getUri(String command, Consumer<URIBuilder> parameters) {
try {
URIBuilder builder = new URIBuilder(this.basePath + "/" + command);
this.baseRequestModifier.stream()
.map(requestModifier -> requestModifier.uri)
.filter(Objects::nonNull)
.forEach(modifier -> modifier.accept(builder));
parameters.accept(builder);
return builder.build();
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
private HttpRequest.Builder getRequestBuilder(URI endpoint) {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(endpoint)
.header("User-Agent", Main.instance().getServer().getBukkitVersion())
.header("Content-Type", "application/json");
this.baseRequestModifier.stream()
.map(requestModifier -> requestModifier.header)
.filter(Objects::nonNull)
.forEach(modifier -> modifier.accept(builder));
return builder;
}
private <TResponse> ReqResp<TResponse> execute(HttpRequest request, Class<TResponse> clazz, Object original) {
ReqResp<String> rawResponse = this.sendHttp(request);
Main.logger().info(String.format(
"Request: %s\nRequest-Data: %s\nResponse: %s",
request,
this.gson.toJson(original),
rawResponse
));
return new ReqResp<>(rawResponse.status(), this.gson.fromJson(rawResponse.data(), clazz));
}
private ReqResp<String> sendHttp(HttpRequest request) {
try(HttpClient client = HttpClient.newHttpClient()) {
this.validateThread(request.uri().getPath());
HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
return new ReqResp<>(httpResponse.statusCode(), httpResponse.body());
} catch(IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,30 @@
package eu.mhsl.craftattack.spawn.core.api.client;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.mhsl.craftattack.spawn.core.Main;
import org.bukkit.Bukkit;
import java.net.URI;
public abstract class Repository {
protected URI basePath;
protected Gson gson;
public Repository(URI basePath) {
this.basePath = basePath;
this.gson = new GsonBuilder()
.serializeNulls()
.create();
}
protected void validateThread(String commandName) {
if(!Bukkit.isPrimaryThread()) return;
Main.logger().warning(String.format(
"Repository '%s' was called synchronously with command '%s'!",
this.getClass().getSimpleName(),
commandName
));
}
}

View File

@ -0,0 +1,52 @@
package eu.mhsl.craftattack.spawn.core.api.client;
import eu.mhsl.craftattack.spawn.core.Main;
import org.apache.commons.lang3.NotImplementedException;
import org.reflections.Reflections;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Set;
public class RepositoryLoader {
private final List<Repository> repositories;
/**
* Defines a repository as an abstraction and will not be loaded by this RepositoryLoader
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Abstraction {
}
public RepositoryLoader() {
Reflections reflections = new Reflections(Main.projectPackage);
Set<Class<? extends Repository>> repositories = reflections.getSubTypesOf(Repository.class);
this.repositories = repositories.stream()
.filter(repository -> !repository.isAnnotationPresent(Abstraction.class))
.map(repository -> {
try {
return (Repository) repository.getDeclaredConstructor().newInstance();
} catch(InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
})
.toList();
}
public <T> T getRepository(Class<T> clazz) {
//noinspection unchecked
return this.repositories.stream()
.filter(clazz::isInstance)
.map(repository -> (T) repository)
.findFirst()
.orElseThrow(() -> new NotImplementedException(String.format("Repository '%s' not found!", clazz.getSimpleName())));
}
public List<Repository> getRepositories() {
return this.repositories;
}
}

View File

@ -0,0 +1,16 @@
package eu.mhsl.craftattack.spawn.core.api.client;
import com.google.gson.Gson;
import java.lang.reflect.Type;
public record ReqResp<TData>(int status, TData data) {
public ReqResp<?> convertToTypeToken(Type type) {
var gson = new Gson();
return new ReqResp<>(this.status, gson.fromJson(gson.toJson(this.data), type));
}
@SuppressWarnings("unchecked")
public <T> T cast() {
return (T) this;
}
}

View File

@ -0,0 +1,76 @@
package eu.mhsl.craftattack.spawn.core.api.server;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.configuration.ConfigurationSection;
import spark.Request;
import spark.Spark;
import java.util.function.Function;
import java.util.function.Supplier;
public class HttpServer {
private final ConfigurationSection apiConf = Main.instance().getConfig().getConfigurationSection("api");
protected final Gson gson = new Gson();
public static Object nothing = null;
public HttpServer() {
Spark.port(8080);
Spark.get("/ping", (request, response) -> System.currentTimeMillis());
Main.instance().getAppliances().forEach(appliance -> appliance.httpApi(new ApiBuilder(appliance)));
}
public record Response(Status status, Object error, Object response) {
public enum Status {
FAILURE,
SUCCESS
}
}
public class ApiBuilder {
@FunctionalInterface
public interface RequestProvider<TParsed, TOriginal, TResponse> {
TResponse apply(TParsed parsed, TOriginal original);
}
private final String applianceName;
private ApiBuilder(Appliance appliance) {
this.applianceName = appliance.getClass().getSimpleName().toLowerCase();
}
public void get(String path, Function<Request, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public void rawPost(String path, Function<Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public <TRequest> void post(String path, Class<TRequest> clazz, RequestProvider<TRequest, Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> {
Main.instance().getLogger().info(req.body());
TRequest parsed = HttpServer.this.gson.fromJson(req.body(), clazz);
return this.process(() -> onCall.apply(parsed, req));
});
}
public String buildRoute(String path) {
return String.format("/api/%s/%s", this.applianceName, path);
}
private String process(Supplier<Object> exec) {
HttpServer.Response response;
try {
response = new Response(Response.Status.SUCCESS, null, exec.get());
} catch(Exception e) {
response = new Response(Response.Status.FAILURE, e, null);
}
return HttpServer.this.gson.toJson(response);
}
}
}

View File

@ -0,0 +1,132 @@
package eu.mhsl.craftattack.spawn.core.appliance;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.client.Repository;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Any implementation of this class can be seen as a "sub-plugin" with its own event handlers and commands.
* Appliances can be enabled or disabled independent of other appliances
*/
public abstract class Appliance {
private String localConfigPath;
private List<Listener> listeners;
private List<ApplianceCommand<?>> commands;
public Appliance() {
}
/**
* Use this constructor to specify a config sub-path for use with the localConfig() method.
*
* @param localConfigPath sub path, if not found, the whole config will be used
*/
public Appliance(String localConfigPath) {
this.localConfigPath = localConfigPath;
}
/**
* Provides a list of listeners for the appliance. All listeners will be automatically registered.
*
* @return List of listeners
*/
@NotNull
protected List<Listener> listeners() {
return new ArrayList<>();
}
/**
* Provides a list of commands for the appliance. All commands will be automatically registered.
*
* @return List of commands
*/
@NotNull
protected List<ApplianceCommand<?>> commands() {
return new ArrayList<>();
}
/**
* Called on initialization to add all needed API Routes.
* The routeBuilder can be used to get the correct Path prefixes
*
* @param apiBuilder holds data for needed route prefixes.
*/
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
}
/**
* Provides a localized config section. Path can be set in appliance constructor.
*
* @return Section of configuration for your appliance
*/
@NotNull
public ConfigurationSection localConfig() {
return Optional.ofNullable(Configuration.cfg.getConfigurationSection(this.localConfigPath))
.orElseGet(() -> Configuration.cfg.createSection(this.localConfigPath));
}
public void onEnable() {
}
public void onDisable() {
}
public void initialize(@NotNull JavaPlugin plugin) {
this.listeners = this.listeners();
this.commands = this.commands();
this.listeners.forEach(listener -> Bukkit.getPluginManager().registerEvents(listener, plugin));
this.commands.forEach(command -> this.setCommandExecutor(plugin, command.commandName, command));
}
public void destruct(@NotNull JavaPlugin plugin) {
this.listeners.forEach(HandlerList::unregisterAll);
}
protected <T extends Appliance> T queryAppliance(Class<T> clazz) {
return Main.instance().getAppliance(clazz);
}
protected <T extends Repository> T queryRepository(Class<T> clazz) {
return Main.instance().getRepositoryLoader().getRepository(clazz);
}
private void setCommandExecutor(JavaPlugin plugin, String name, ApplianceCommand<?> executor) {
try {
PluginCommand command = this.createPluginCommand(name, plugin);
command.setExecutor(executor);
command.setTabCompleter(executor);
plugin.getServer().getCommandMap().register(plugin.getName(), command);
} catch(Exception e) {
throw new RuntimeException(String.format("Failed to register command '%s'", name), e);
}
}
private PluginCommand createPluginCommand(String name, JavaPlugin plugin) throws Exception {
Constructor<PluginCommand> constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class);
constructor.setAccessible(true);
return constructor.newInstance(name, plugin);
}
public List<Listener> getListeners() {
return this.listeners;
}
public List<ApplianceCommand<?>> getCommands() {
return this.commands;
}
}

View File

@ -1,8 +1,8 @@
package eu.mhsl.craftattack.spawn.appliance; package eu.mhsl.craftattack.spawn.core.appliance;
import eu.mhsl.craftattack.spawn.core.Main;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -17,9 +17,10 @@ import java.util.Optional;
/** /**
* Utility class which enables command name definition over a constructor. * Utility class which enables command name definition over a constructor.
*/ */
public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSupplier<T> implements TabCompleter, CommandExecutor { public abstract class ApplianceCommand<T extends Appliance> extends CachedApplianceSupplier<T> implements TabCompleter, CommandExecutor {
public String commandName; public String commandName;
protected Component errorMessage = Component.text("Fehler: ").color(NamedTextColor.RED); protected Component errorMessage = Component.text("Fehler: ").color(NamedTextColor.RED);
public ApplianceCommand(String command) { public ApplianceCommand(String command) {
this.commandName = command; this.commandName = command;
} }
@ -32,12 +33,12 @@ public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSup
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
try { try {
execute(sender, command, label, args); this.execute(sender, command, label, args);
} catch (Error e) { } catch(Error e) {
sender.sendMessage(errorMessage.append(Component.text(e.getMessage()))); sender.sendMessage(this.errorMessage.append(Component.text(e.getMessage())));
} catch (Exception e) { } catch(Exception e) {
sender.sendMessage(errorMessage.append(Component.text("Interner Fehler"))); sender.sendMessage(this.errorMessage.append(Component.text("Interner Fehler")));
Bukkit.getLogger().warning("Error executing appliance command " + commandName + ": " + e.getMessage()); Main.logger().warning("Error executing appliance command " + this.commandName + ": " + e.getMessage());
e.printStackTrace(System.err); e.printStackTrace(System.err);
return false; return false;
} }
@ -50,10 +51,10 @@ public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSup
} }
protected List<String> tabCompleteReducer(List<String> response, String[] args) { protected List<String> tabCompleteReducer(List<String> response, String[] args) {
return response.stream().filter(s -> s.startsWith(args[args.length-1])).toList(); return response.stream().filter(s -> s.startsWith(args[args.length - 1])).toList();
} }
protected abstract void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args); protected abstract void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception;
/** /**
* Utility class for command which can only be used as a Player. You can access the executing player with the getPlayer() method. * Utility class for command which can only be used as a Player. You can access the executing player with the getPlayer() method.
@ -61,6 +62,7 @@ public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSup
public static abstract class PlayerChecked<T extends Appliance> extends ApplianceCommand<T> { public static abstract class PlayerChecked<T extends Appliance> extends ApplianceCommand<T> {
private Player player; private Player player;
private Component notPlayerMessage = Component.text("Dieser Command kann nur von Spielern ausgeführt werden!").color(NamedTextColor.RED); private Component notPlayerMessage = Component.text("Dieser Command kann nur von Spielern ausgeführt werden!").color(NamedTextColor.RED);
public PlayerChecked(String command) { public PlayerChecked(String command) {
super(command); super(command);
} }
@ -78,7 +80,7 @@ public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSup
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(!(sender instanceof Player)) { if(!(sender instanceof Player)) {
sender.sendMessage(notPlayerMessage); sender.sendMessage(this.notPlayerMessage);
return false; return false;
} }
this.player = (Player) sender; this.player = (Player) sender;

View File

@ -1,12 +1,13 @@
package eu.mhsl.craftattack.spawn.appliance; package eu.mhsl.craftattack.spawn.core.appliance;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
/** /**
* Utility class which provides a specific, type save appliance. * Utility class which provides a specific, type save appliance.
* You can access the appliance with the protected 'appliance' field. * You can access the appliance with the protected 'appliance' field.
*
* @param <T> the type of your appliance * @param <T> the type of your appliance
*/ */
public abstract class ApplianceListener<T extends Appliance> extends ApplianceSupplier<T> implements Listener { public abstract class ApplianceListener<T extends Appliance> extends CachedApplianceSupplier<T> implements Listener {
} }

View File

@ -0,0 +1,16 @@
package eu.mhsl.craftattack.spawn.core.appliance;
import eu.mhsl.craftattack.spawn.core.Main;
public class CachedApplianceSupplier<T extends Appliance> implements IApplianceSupplier<T> {
private final T appliance;
public CachedApplianceSupplier() {
this.appliance = Main.instance().getAppliance(Main.getApplianceType(this.getClass()));
}
@Override
public T getAppliance() {
return this.appliance;
}
}

View File

@ -1,4 +1,4 @@
package eu.mhsl.craftattack.spawn.appliance; package eu.mhsl.craftattack.spawn.core.appliance;
public interface IApplianceSupplier<T extends Appliance> { public interface IApplianceSupplier<T extends Appliance> {
T getAppliance(); T getAppliance();

View File

@ -1,4 +1,4 @@
package eu.mhsl.craftattack.spawn.config; package eu.mhsl.craftattack.spawn.core.config;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
@ -10,12 +10,12 @@ public class ConfigUtil {
public static class Position { public static class Position {
public static Location paseLocation(ConfigurationSection section) { public static Location paseLocation(ConfigurationSection section) {
return new Location( return new Location(
Bukkit.getWorld(Optional.ofNullable(section.getString("world")).orElse("world")), Bukkit.getWorld(Optional.ofNullable(section.getString("world")).orElse("world")),
section.getDouble("x"), section.getDouble("x"),
section.getDouble("y"), section.getDouble("y"),
section.getDouble("z"), section.getDouble("z"),
(float) section.getDouble("yaw"), (float) section.getDouble("yaw"),
(float) section.getDouble("pitch") (float) section.getDouble("pitch")
); );
} }

View File

@ -1,7 +1,7 @@
package eu.mhsl.craftattack.spawn.config; package eu.mhsl.craftattack.spawn.core.config;
import eu.mhsl.craftattack.spawn.Main; import eu.mhsl.craftattack.spawn.core.Main;
import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -11,16 +11,18 @@ public class Configuration {
private static final String configName = "config.yml"; private static final String configName = "config.yml";
private static final File configFile = new File(Main.instance().getDataFolder().getAbsolutePath() + "/" + configName); private static final File configFile = new File(Main.instance().getDataFolder().getAbsolutePath() + "/" + configName);
public static FileConfiguration cfg; public static FileConfiguration cfg;
public static ConfigurationSection pluginConfig;
public static void readConfig() { public static void readConfig() {
cfg = YamlConfiguration.loadConfiguration(configFile); cfg = YamlConfiguration.loadConfiguration(configFile);
pluginConfig = cfg.getConfigurationSection("plugin");
} }
public static void saveChanges() { public static void saveChanges() {
try { try {
cfg.save(configFile); cfg.save(configFile);
} catch (Exception e) { } catch(Exception e) {
Bukkit.getLogger().warning("Could not save configuration: " + e.getMessage()); Main.logger().warning("Could not save configuration: " + e.getMessage());
} }
} }

View File

@ -0,0 +1,54 @@
package eu.mhsl.craftattack.spawn.core.util;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class IteratorUtil {
public static void worlds(Consumer<World> world) {
worlds(w -> w, world);
}
public static <T> void worlds(Function<World, T> selector, Consumer<T> selected) {
Bukkit.getWorlds().forEach(world -> selected.accept(selector.apply(world)));
}
public static void onlinePlayers(Consumer<Player> player) {
Bukkit.getOnlinePlayers().forEach(player);
}
public static void setGameRules(Map<GameRule<Boolean>, Boolean> rules, boolean inverse) {
rules.forEach((gameRule, value) -> IteratorUtil.worlds(world -> world.setGameRule(gameRule, value ^ inverse)));
}
public static void times(int times, Runnable callback) {
IntStream.range(0, times).forEach(value -> callback.run());
}
public static <T> void iterateListInGlobal(int sectionStart, List<T> list, BiConsumer<Integer, T> callback) {
IntStream.range(sectionStart, sectionStart + list.size())
.forEach(value -> callback.accept(value, list.get(value - sectionStart)));
}
public static void iterateLocalInGlobal(int sectionStart, int localLength, BiConsumer<Integer, Integer> callback) {
IntStream.range(sectionStart, sectionStart + localLength)
.forEach(value -> callback.accept(value, value + sectionStart));
}
public static <T> List<T> expandList(List<T> list, int targetSize, T defaultValue) {
return Stream.concat(
list.stream(),
Stream.generate(() -> defaultValue).limit(Math.max(0, targetSize - list.size()))
).collect(Collectors.toList());
}
}

View File

@ -1,8 +1,8 @@
package eu.mhsl.craftattack.spawn.util; package eu.mhsl.craftattack.spawn.core.util;
public class NumberUtil { public class NumberUtil {
public static double map(double oldValue, double oldMin, double oldMax, double newMin, double newMax) { public static double map(double oldValue, double oldMin, double oldMax, double newMin, double newMax) {
double out = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin; double out = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin;
if(out > newMax) out = newMax; if(out > newMax) out = newMax;
if(out < newMin) out = newMin; if(out < newMin) out = newMin;

View File

@ -1,7 +1,8 @@
package eu.mhsl.craftattack.spawn.util; package eu.mhsl.craftattack.spawn.core.util.entity;
import eu.mhsl.craftattack.spawn.config.ConfigUtil; import eu.mhsl.craftattack.spawn.core.config.ConfigUtil;
import eu.mhsl.craftattack.spawn.config.Configuration; import eu.mhsl.craftattack.spawn.core.config.Configuration;
import eu.mhsl.craftattack.spawn.core.util.world.ChunkUtils;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
@ -22,8 +23,8 @@ public class DisplayVillager {
ChunkUtils.loadChunkAtLocation(this.location); ChunkUtils.loadChunkAtLocation(this.location);
this.villager = (Villager) this.location.getWorld().getEntity(uuid); this.villager = (Villager) this.location.getWorld().getEntity(uuid);
Objects.requireNonNull(this.villager); Objects.requireNonNull(this.villager);
} catch (NullPointerException | IllegalArgumentException e) { } catch(NullPointerException | IllegalArgumentException e) {
this.villager = getBaseVillager(); this.villager = this.getBaseVillager();
villagerCreator.accept(this.villager); villagerCreator.accept(this.villager);
} }
@ -31,7 +32,7 @@ public class DisplayVillager {
} }
public Villager getVillager() { public Villager getVillager() {
return villager; return this.villager;
} }
private Villager getBaseVillager() { private Villager getBaseVillager() {
@ -50,26 +51,27 @@ public class DisplayVillager {
public static class ConfigBound { public static class ConfigBound {
private final DisplayVillager villager; private final DisplayVillager villager;
private final ConfigurationSection config; private final ConfigurationSection config;
public ConfigBound(ConfigurationSection configurationSection, Consumer<Villager> villagerCreator) { public ConfigBound(ConfigurationSection configurationSection, Consumer<Villager> villagerCreator) {
this.config = configurationSection; this.config = configurationSection;
Location location = ConfigUtil.Position.paseLocation(Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation"))); Location location = ConfigUtil.Position.paseLocation(Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation")));
this.villager = new DisplayVillager( this.villager = new DisplayVillager(
UUID.fromString(this.config.getString("uuid", UUID.randomUUID().toString())), UUID.fromString(this.config.getString("uuid", UUID.randomUUID().toString())),
location, location,
villager -> { villager -> {
this.config.set("uuid", villager.getUniqueId().toString()); this.config.set("uuid", villager.getUniqueId().toString());
Configuration.saveChanges(); Configuration.saveChanges();
villagerCreator.accept(villager); villagerCreator.accept(villager);
} }
); );
} }
public void updateLocation(Location location) { public void updateLocation(Location location) {
ConfigUtil.Position.writeLocation( ConfigUtil.Position.writeLocation(
Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation")), Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation")),
location location
); );
Configuration.saveChanges(); Configuration.saveChanges();
@ -77,11 +79,11 @@ public class DisplayVillager {
} }
public Villager getVillager() { public Villager getVillager() {
return villager.getVillager(); return this.villager.getVillager();
} }
public UUID getUniqueId() { public UUID getUniqueId() {
return getVillager().getUniqueId(); return this.getVillager().getUniqueId();
} }
} }
} }

View File

@ -0,0 +1,33 @@
package eu.mhsl.craftattack.spawn.core.util.entity;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
public class PlayerUtils {
public static void resetStatistics(Player player) {
for(Statistic statistic : Statistic.values()) {
for(Material material : Material.values()) {
try {
player.setStatistic(statistic, material, 0);
} catch(IllegalArgumentException e) {
break;
}
}
for(EntityType entityType : EntityType.values()) {
try {
player.setStatistic(statistic, entityType, 0);
} catch(IllegalArgumentException e) {
break;
}
}
try {
player.setStatistic(statistic, 0);
} catch(IllegalArgumentException ignored) {
}
}
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.core.util.inventory;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.UUID;
public class HeadBuilder {
public static ItemStack getCustomTextureHead(String base64) {
ItemStack head = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) head.getItemMeta();
PlayerProfile profile = Bukkit.createProfile(UUID.nameUUIDFromBytes(base64.getBytes()), null);
profile.setProperty(new ProfileProperty("textures", base64));
meta.setPlayerProfile(profile);
head.setItemMeta(meta);
return head;
}
}

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