Compare commits

..

63 Commits

Author SHA1 Message Date
besiece f615dc867a Rerunning my first test, to verify if my install worked properly 2026-08-13 13:09:14 +02:00
besiece 673bae4d50 Fixing my first test, to verify if my install worked properly 2026-08-13 10:33:38 +02:00
besiece 6ad9407cb4 Creating a first test, to verify if my install worked properly 2026-08-13 10:14:38 +02:00
Pupsi 9dad34719b removed Exception throw from CustomAdvancements, fixed time reset at project start 2026-07-15 16:54:44 +02:00
Pupsi 323e316f0f fixed bloodmoon sleep / time skip issue, added endermite hordes for end, fixed server restart during bloodmoon issue 2026-01-17 17:09:54 +01:00
Pupsi 0a16e1e049 added missing setting check for bloodmoon warning messages 2026-01-09 19:25:10 +01:00
MineTec c371893407 added support for portable stonecutter alongside crafting table, renamed listener for clarity 2026-01-02 22:29:27 +01:00
MineTec f2bf8f1858 fixed username validation to allow exceptions for Bedrock players due to specific naming constraints 2025-12-29 09:05:57 +01:00
MineTec a0a33f1f56 filtered null and duplicate player names in ReportCommand responses for improved reliability 2025-12-28 17:36:01 +01:00
Pupsi 164a160dbb fixed bloodmoon hordes for players with deactivated setting 2025-12-28 17:21:33 +01:00
MineTec efdbc6fe9f updated feedback link behavior, disabled outdated appliances, adjusted infection spawn and silverfish mechanics 2025-12-28 11:40:21 +01:00
Pupsi 7ac02b4ec4 added missing return for /event 2025-12-27 11:35:20 +01:00
MineTec 93971650ce adjusted strike punishment durations for better balance and scaling 2025-12-27 11:08:47 +01:00
MineTec 336e3f934c remove wrong location description 2025-12-27 00:47:13 +01:00
Pupsi 91e350ee62 disabled worldmuseum for craftattack 8 2025-12-26 19:50:59 +01:00
MineTec 8736e78adf Merge remote-tracking branch 'origin/master' 2025-12-25 22:49:19 +01:00
MineTec 329ec8e1da renamed adminPermission constant to adminmarker for improved clarity and consistency 2025-12-25 22:49:15 +01:00
Pupsi ebf392b024 fixed bloodmoon progress bar trying updates when inactive 2025-12-24 14:26:23 +01:00
Pupsi 33d00e97c6 fixed recovery compass at project start 2025-12-24 01:07:53 +01:00
Pupsi 1fdcc11211 Merge pull request 'master-big-events' (#11) from master-big-events into master
Reviewed-on: #11
2025-12-23 23:54:14 +00:00
Pupsi e0ed7ecdf5 Merge branch 'master' into master-big-events 2025-12-23 23:53:42 +00:00
Pupsi 2ca97c88fc made score staying the highest ever reached 2025-12-24 00:49:50 +01:00
Pupsi fc502e3da8 fixed player join when no event is loaded 2025-12-24 00:16:22 +01:00
Pupsi 5e3db1e78c fixed event commands, removed infinite saturation, added player join check 2025-12-23 23:40:58 +01:00
MineTec fc6fd9ebb5 Merge branch 'master-big-events' 2025-12-23 22:34:51 +01:00
MineTec 84edbcc0e4 Merge branch 'master-big-events' 2025-12-23 22:34:33 +01:00
Pupsi 9ec883d6ad made Scoreboard more beautiful 2025-12-23 22:33:32 +01:00
Pupsi 2b0c7c1a9e added countdown and sounds 2025-12-23 21:43:25 +01:00
MineTec df39093c69 enhanced Outlawed PVP activation message to include cooldown duration; fixed minor typo in forced status message 2025-12-23 21:32:35 +01:00
MineTec dfbf87dcd4 updated ProjectStart to adjust coordinates, music, and glass material; refined player inventory handling logic; renamed vogelfrei to PVP in Outlawed messages for clarity 2025-12-23 21:07:32 +01:00
MineTec 8953a19400 added Bedrock player warning in joinEvent method to handle potential compatibility issues 2025-12-23 19:52:17 +01:00
MineTec 85065bcc73 made SignEditListener formatting consistent and optimized ReportJoinListener to handle admin notifications asynchronously 2025-12-23 19:14:53 +01:00
Pupsi 2209b42766 fixed wrong reports info filtering 2025-12-23 19:11:59 +01:00
Pupsi bf11bb0b70 fixed wrong reports info filtering 2025-12-23 19:05:53 +01:00
Pupsi b98d33af40 added report reminder on admin join 2025-12-23 18:56:30 +01:00
Pupsi 0d6b21701f Merge remote-tracking branch 'origin/master' 2025-12-23 15:50:41 +01:00
Pupsi b3240cdb22 fixed maximum of 8 settings per category 2025-12-23 15:50:23 +01:00
MineTec 89c1c4335b refined SignEditListener to filter illegal characters more robustly by normalizing input and introducing specific character whitelists 2025-12-22 21:41:34 +01:00
MineTec 71f2da8e99 introduced AntiIllegalSignCharacters appliance to prevent usage of illegal characters in sign edits 2025-12-22 18:44:22 +01:00
MineTec a257b604ea updated InventoryTrackerListener to monitor InventoryOpenEvent with high priority and prevent processing of cancelled events 2025-12-21 09:32:40 +01:00
Pupsi ef153d5d8f added invincibility before event start, added join craftattack command, added player teleport, portal and pvp listeners to deathrun 2025-12-20 23:13:14 +01:00
Pupsi bd883a4fa1 added big events in craftattack plugin 2025-12-20 18:23:06 +01:00
MineTec ac7e04829e introduced ReportCreatedListener, updated ReportCreatedEvent to use UUIDs for reporter and reported, and improved admin notifications for new reports 2025-12-20 17:46:02 +01:00
Pupsi 5cda58408a Merge remote-tracking branch 'origin/master' into master-big-events 2025-12-20 17:45:26 +01:00
MineTec 9767896cde updated OutlawedCommand and Outlawed to rename vogelfrei to PVP, adjusted related messages accordingly 2025-12-20 11:51:30 +01:00
Pupsi f0e0cfbb85 fixed scoreboard order 2025-12-19 14:33:01 +01:00
Pupsi de112f7e13 added timer 2025-12-19 14:04:36 +01:00
Pupsi 215259c6b9 added 'this' qualifier in Bloodmoon 2025-12-19 13:01:01 +01:00
Pupsi 36520a87f9 moved automatic scoreboard update start to EventController 2025-12-19 12:58:10 +01:00
Pupsi 914aaff10b added deathrun listener for border; fixed listener problem when disabling appliance 2025-12-13 16:02:23 +01:00
MineTec e015bbb356 added unsuspectedKick and crashKick features to Kick appliance; updated AcInform to include related commands 2025-12-13 15:00:48 +01:00
Pupsi ec262710ec removed possibility for multiple events 2025-12-13 13:50:42 +01:00
MineTec 1ac19014c1 introduced AntiInventoryMove appliance with related listeners and optimized admin notification logic in AcInform 2025-12-13 13:45:37 +01:00
Pupsi dd1518fce4 split Event into Scorable and Event; removed AbstractEvent; added list, select and unselect commands 2025-12-12 19:30:21 +01:00
Pupsi 04cb233604 WIP: moved scores and score updates to EventScoreboardBuilder 2025-12-12 12:09:25 +01:00
Pupsi 2ff95f8450 changed scoreboard package name to lowercase 2025-12-02 12:52:18 +01:00
Pupsi 6ed48895ca made appliance ignore destruct when no listeners 2025-12-01 22:29:13 +01:00
Pupsi bff8cf24cd added AbstractEvent and basic Scoreboard functionality 2025-12-01 22:28:15 +01:00
Pupsi 212f84b6de added start, stop and getScore methods to Event 2025-11-30 18:07:15 +01:00
Pupsi f7430c8fc8 added possibility to disable appliances 2025-11-28 21:08:27 +01:00
Pupsi c81a2d2161 started big events 2025-11-28 16:52:42 +01:00
MineTec 2087b4c379 Merge pull request 'develop-bloodmoon' (#10) from develop-bloodmoon into master
Reviewed-on: #10
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2025-11-23 14:03:06 +00:00
Pupsi 4d9548aafc resolved pr comments 2025-11-23 15:00:16 +01:00
63 changed files with 1644 additions and 180 deletions
@@ -23,4 +23,8 @@ public class CraftAttackReportRepository extends ReportRepository {
ReportUrl.class
);
}
public ReqResp<AllReports> queryAllReports() {
return this.get("reports", AllReports.class);
}
}
@@ -21,6 +21,11 @@ public abstract class ReportRepository extends HttpRepository {
public record ReportUrl(@NotNull String url) {
}
public enum Status {
open,
closed,
}
public record PlayerReports(
List<Report> from_self,
List<Report> to_self
@@ -31,11 +36,17 @@ public abstract class ReportRepository extends HttpRepository {
@Nullable Long created,
@Nullable Status status,
@NotNull String url
) {
public enum Status {
open,
closed,
}
}
) { }
}
public record AllReports(List<Report> reports) {
public record Report(
@NotNull UUID reporter,
@Nullable UUID reported,
@NotNull String reason,
@Nullable Long created,
@Nullable Status status,
@NotNull String url
) { }
}
}
@@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class AdminMarker extends Appliance implements DisplayName.Colored {
public final static String adminPermission = "admin";
public final static String adminPermission = "adminmarker";
@Override
public @Nullable TextColor getNameColor(Player player) {
@@ -19,7 +19,7 @@ public class HelpCommand extends ApplianceCommand<Help> {
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))
" Weitere Informationen zu Funktionen und Befehlen erhältst du zudem am Spawn.", NamedTextColor.GRAY))
);
}
}
@@ -1,6 +1,8 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.common.api.repositories.ReportRepository;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners.ReportCreatedListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners.ReportJoinListener;
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;
@@ -16,6 +18,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -132,7 +135,7 @@ public class Report extends Appliance {
}
Function<List<ReportRepository.PlayerReports.Report>, List<ReportRepository.PlayerReports.Report>> filterClosed = reports -> reports.stream()
.filter(report -> Objects.equals(report.status(), ReportRepository.PlayerReports.Report.Status.closed))
.filter(report -> Objects.equals(report.status(), ReportRepository.Status.closed))
.toList();
List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed();
@@ -178,6 +181,35 @@ public class Report extends Appliance {
issuer.sendMessage(component.build());
}
public void sendReportsInfo(Player player) {
ReqResp<ReportRepository.AllReports> allReportsResponse = this.queryRepository(CraftAttackReportRepository.class).queryAllReports();
if(allReportsResponse.status() != 200) {
Main.logger().warning("Failed to request Reports: " + allReportsResponse.status());
return;
}
List<ReportRepository.AllReports.Report> allOpenReports = allReportsResponse.data().reports().stream()
.filter(report -> report.status() == null && report.created() != null)
.toList();
if(allOpenReports.isEmpty()) return;
player.sendMessage(
Component.text("Hey, es gibt noch ", NamedTextColor.GOLD)
.append(Component.text(allOpenReports.size(), NamedTextColor.RED))
.append(Component.text(" unbearbeitete Reports!", NamedTextColor.GOLD))
);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new ReportCreatedListener(),
new ReportJoinListener()
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
@@ -13,6 +13,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -46,7 +47,7 @@ class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
response = Stream.concat(
Bukkit.getOnlinePlayers().stream().map(Player::getName),
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
).toList();
).filter(Objects::nonNull).distinct().toList();
}
if(args.length == 2) {
@@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.event.ReportCreatedEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.event.EventHandler;
public class ReportCreatedListener extends ApplianceListener<Report> {
@EventHandler
public void onReport(ReportCreatedEvent event) {
OfflinePlayer reporter = Bukkit.getOfflinePlayer(event.getReport().reporter());
OfflinePlayer reported = Bukkit.getOfflinePlayer(event.getReport().reported());
Component message = Component.text(
"\uD83D\uDD14 Neuer Report von %s gegen %s: %s".formatted(reporter.getName(), reported.getName(), event.getReport().reason()),
NamedTextColor.YELLOW
);
Bukkit.getOnlinePlayers().stream()
.filter(player -> player.hasPermission("admin"))
.forEach(player -> player.sendMessage(message));
}
}
@@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class ReportJoinListener extends ApplianceListener<Report> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if(!event.getPlayer().hasPermission("admin")) return;
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.getAppliance().sendReportsInfo(event.getPlayer())
);
}
}
@@ -128,12 +128,12 @@ public class Settings extends Appliance {
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) {
if(i % 9 == 0 && i != 0) {
row.incrementAndGet();
}
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
}
row.incrementAndGet();
});
@@ -143,12 +143,12 @@ public class Settings extends Appliance {
.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) {
if(i % 9 == 0 && i != 0) {
row.incrementAndGet();
}
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
}
player.openInventory(inventory);
@@ -5,15 +5,11 @@ import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.Bukkit;
import org.bukkit.entity.Boat;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
@Appliance.Flags(enabled = false)
public class AntiBoatFreecam extends Appliance {
private static final float MAX_YAW_OFFSET = 106.0f;
private final Map<Player, Float> violatedPlayers = new HashMap<>();
public AntiBoatFreecam() {
Bukkit.getScheduler().runTaskTimerAsynchronously(
@@ -27,14 +23,12 @@ public class AntiBoatFreecam extends Appliance {
float yawDelta = wrapDegrees(playerYaw - boatYaw);
if(Math.abs(yawDelta) <= MAX_YAW_OFFSET) return;
this.violatedPlayers.merge(player, 1f, Float::sum);
float violationCount = this.violatedPlayers.get(player);
if(violationCount != 1 && violationCount % 100 != 0) return;
Main.instance().getAppliance(AcInform.class).notifyAdmins(
Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
"internal",
player.getName(),
"illegalBoatLookYaw",
violationCount
yawDelta,
3000
);
}),
1L,
@@ -0,0 +1,16 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalSignCharacters;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AntiIllegalSignCharacters extends Appliance {
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new SignEditListener()
);
}
}
@@ -0,0 +1,77 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalSignCharacters;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.SignChangeEvent;
import java.text.Normalizer;
import java.util.Set;
class SignEditListener extends ApplianceListener<AntiIllegalSignCharacters> {
private static final Set<Integer> ALLOWED_CHARS = Set.of(
(int)' ', (int)'.', (int)',', (int)';', (int)':', (int)'!', (int)'?',
(int)'"', (int)'\'',
(int)'(', (int)')', (int)'[', (int)']', (int)'{', (int)'}',
(int)'-', (int)'_', (int)'+', (int)'=', (int)'/', (int)'\\',
(int)'@', (int)'#', (int)'$', (int)'%', (int)'&', (int)'*',
(int)'<', (int)'>', (int)'|',
(int)'~', (int)'`', (int)'^'
);
private static final Set<Integer> ALLOWED_EXTRA = Set.of(
(int)'Ä', (int)'Ö', (int)'Ü', (int)'ä', (int)'ö', (int)'ü', (int)'ß',
(int)'€', (int)'°', (int)'µ'
);
@EventHandler
public void onSignEdit(SignChangeEvent event) {
for (int i = 0; i < 4; i++) {
Component line = event.line(i);
if (line == null) continue;
String plainString = PlainTextComponentSerializer.plainText().serialize(line);
plainString = Normalizer.normalize(plainString, Normalizer.Form.NFC);
String cleaned = filterAllowed(plainString);
event.line(i, Component.text(cleaned));
}
}
private static String filterAllowed(String s) {
StringBuilder out = new StringBuilder(s.length());
for (int off = 0; off < s.length(); ) {
int cp = s.codePointAt(off);
off += Character.charCount(cp);
if (isForbidden(cp)) continue;
if (Character.isLetterOrDigit(cp)) {
out.appendCodePoint(cp);
continue;
}
if (ALLOWED_CHARS.contains(cp) || ALLOWED_EXTRA.contains(cp)) {
out.appendCodePoint(cp);
}
}
return out.toString();
}
private static boolean isForbidden(int cp) {
// Surrogates / invalid
if (cp >= 0xD800 && cp <= 0xDFFF) return true;
// Private Use Area (Mod/Pack-Icons/Placeholder)
if (cp >= 0xE000 && cp <= 0xF8FF) return true;
// Zero-width and control characters
if (cp == 0x200B || cp == 0x200C || cp == 0x200D || cp == 0xFEFF) return true;
// BiDi-Steuerzeichen
if (cp >= 0x202A && cp <= 0x202E) return true;
if (cp >= 0x2066 && cp <= 0x2069) return true;
return cp == '§';
}
}
@@ -0,0 +1,39 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import net.kyori.adventure.util.Ticks;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Appliance.Flags(enabled = false)
public class AntiInventoryMove extends Appliance {
private static final long errorTimeMargin = Ticks.SINGLE_TICK_DURATION_MS * 2;
private final Map<UUID, Long> invOpen = new ConcurrentHashMap<>();
public void setInvOpen(Player player, boolean open) {
if(open)
this.invOpen.put(player.getUniqueId(), System.currentTimeMillis());
else
this.invOpen.remove(player.getUniqueId());
}
public boolean hasInventoryOpen(Player player) {
if(!this.invOpen.containsKey(player.getUniqueId())) return false;
return this.invOpen.get(player.getUniqueId()) < System.currentTimeMillis() - errorTimeMargin;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new InventoryTrackerListener(),
new InInventoryMoveListener()
);
}
}
@@ -0,0 +1,21 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInputEvent;
class InInventoryMoveListener extends ApplianceListener<AntiInventoryMove> {
@EventHandler
public void onInput(PlayerInputEvent event) {
if(!this.getAppliance().hasInventoryOpen(event.getPlayer())) return;
Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
"internal",
event.getPlayer().getName(),
"inInventoryMove",
-1f,
3000
);
}
}
@@ -0,0 +1,23 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
class InventoryTrackerListener extends ApplianceListener<AntiInventoryMove> {
@EventHandler(priority = EventPriority.MONITOR)
public void onOpen(InventoryOpenEvent event) {
if(!(event.getPlayer() instanceof Player player)) return;
if(event.isCancelled()) return;
this.getAppliance().setInvOpen(player, true);
}
@EventHandler
public void onClose(InventoryCloseEvent event) {
if(!(event.getPlayer() instanceof Player player)) return;
this.getAppliance().setInvOpen(player, false);
}
}
@@ -11,14 +11,20 @@ import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AcInform extends Appliance {
private final Map<String, Map<String, Long>> violationSlowdowns = new ConcurrentHashMap<>();
public void processCommand(@NotNull String[] args) {
String anticheatName = null;
String playerName = null;
String checkName = null;
Float violationCount = null;
int notifyEvery = 0;
for(int i = 0; i < args.length; i++) {
if(!args[i].startsWith("--")) continue;
@@ -36,13 +42,32 @@ public class AcInform extends Appliance {
case "--playerName" -> playerName = value;
case "--check" -> checkName = value;
case "--violationCount" -> violationCount = value.isEmpty() ? null : Float.valueOf(value);
case "--notifyEvery" -> notifyEvery = Integer.parseInt(value);
}
}
if(notifyEvery == 0) {
this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
} else {
this.slowedNotifyAdmins(anticheatName, playerName, checkName, violationCount, notifyEvery);
}
}
public void slowedNotifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float violationCount, int notifyEvery) {
this.violationSlowdowns.putIfAbsent(playerName, new HashMap<>());
var slowdowns = this.violationSlowdowns.get(playerName);
if(slowdowns.containsKey(checkName)) {
if(slowdowns.get(checkName) > System.currentTimeMillis() - notifyEvery) return;
}
this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
}
public void notifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float violationCount) {
this.violationSlowdowns.putIfAbsent(playerName, new HashMap<>());
this.violationSlowdowns.get(playerName).put(checkName, System.currentTimeMillis());
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text();
NamedTextColor textColor = NamedTextColor.GRAY;
@@ -85,28 +110,42 @@ public class AcInform extends Appliance {
Component.newline()
.append(Component.text("", NamedTextColor.GRAY))
.append(Component.text("[", NamedTextColor.GRAY))
.append(Component.text("Report", NamedTextColor.GOLD))
.append(Component.text("\uD83D\uDCD6", 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("\u23F1", 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("\uD83E\uDDB6", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/kickunsuspected %s", playerName)))
);
component.append(
Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("\u2623", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/kickcrash %s", playerName)))
);
component.append(
Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("\uD83D\uDD12", 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("\uD83D\uDC41", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/grim spectate %s", playerName)))
);
@@ -1,9 +1,14 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.kick;
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.core.util.entity.PlayerUtils;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,9 +30,36 @@ public class Kick extends Appliance {
).applyKick(player);
}
public void unsuspectedKick(@NotNull String playerName) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
String material = Material.values()[(int)(Math.random() * Material.values().length)].name();
player.kick(Component.text("java.lang.IllegalStateException: Failed to create model for minecraft:%s".formatted(material)));
}
public void crashKick(@NotNull String playerName) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
PlayerUtils.sendCube(player, 100, Material.ENCHANTING_TABLE.createBlockData());
PlayerUtils.sendCube(player, 5, Material.DIRT.createBlockData());
});
Bukkit.getScheduler().runTaskLater(Main.instance(), () -> player.kick(Component.empty()), Ticks.TICKS_PER_SECOND * 15);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new KickCommand());
return List.of(
new KickCommand(),
new KickUnsuspectedCommand(),
new KickCrashCommand()
);
}
}
@@ -0,0 +1,31 @@
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.List;
public class KickCrashCommand extends ApplianceCommand<Kick> {
public KickCrashCommand() {
super("kickCrash");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length < 1) throw new Error("Es muss ein Spielername angegeben werden!");
this.getAppliance().crashKick(args[0]);
}
@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
);
}
}
@@ -0,0 +1,31 @@
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.List;
public class KickUnsuspectedCommand extends ApplianceCommand<Kick> {
public KickUnsuspectedCommand() {
super("kickUnsuspected");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length < 1) throw new Error("Es muss ein Spielername angegeben werden!");
this.getAppliance().unsuspectedKick(args[0]);
}
@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
);
}
}
@@ -10,6 +10,7 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.reflections.Reflections;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
@@ -49,7 +50,7 @@ public final class Main extends JavaPlugin {
Main.logger().info(String.format("Loaded %d repositories!", this.repositoryLoader.getRepositories().size()));
Main.logger().info("Loading appliances...");
this.appliances = this.findSubtypesOf(Appliance.class).stream()
this.appliances = new ArrayList<>(this.findSubtypesOf(Appliance.class).stream()
.filter(applianceClass -> !disabledAppliances.contains(applianceClass.getSimpleName()))
.filter(appliance -> {
Appliance.Flags flags = appliance.getAnnotation(Appliance.Flags.class);
@@ -63,14 +64,17 @@ public final class Main extends JavaPlugin {
throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e);
}
})
.toList();
.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);
});
this.appliances.stream()
.filter(appliance -> {
Appliance.Flags flags = appliance.getClass().getAnnotation(Appliance.Flags.class);
if(flags == null) return true;
return flags.autoload();
})
.forEach(appliance -> appliance.enableSequence(this));
Main.logger().info(String.format("Initialized %d appliances!", this.appliances.size()));
if(Configuration.pluginConfig.getBoolean("httpServerEnabled", true)) {
@@ -85,17 +89,34 @@ public final class Main extends JavaPlugin {
@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);
});
this.appliances.forEach(appliance -> appliance.disableSequence(this));
HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTasks(this);
Main.logger().info("Disabled " + this.appliances.size() + " appliances!");
}
public Appliance restartAppliance(Class<? extends Appliance> applianceClass) {
Appliance oldAppliance = this.appliances.stream()
.filter(appliance -> appliance.getClass().equals(applianceClass))
.findFirst()
.orElseThrow();
oldAppliance.disableSequence(this);
this.appliances.remove(oldAppliance);
Appliance newAppliance = this.createApplianceInstance(applianceClass);
this.appliances.add(newAppliance);
newAppliance.enableSequence(this);
return newAppliance;
}
private Appliance createApplianceInstance(Class<? extends Appliance> applianceClass) {
try {
return applianceClass.getDeclaredConstructor().newInstance();
} catch(Exception e) {
throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e);
}
}
public <T extends Appliance> T getAppliance(Class<T> clazz) {
return this.appliances.stream()
.filter(clazz::isInstance)
@@ -28,14 +28,16 @@ public class WebsiteHook extends HttpHook {
return HttpServer.nothing;
}));
record CreatedReport(String reporter, String reported, String reason) {}
record CreatedReport(UUID reporter, UUID reported, String reason) {}
this.addAction("report", new JsonAction<>(CreatedReport.class, createdReport -> {
Main.logger().info(String.format("New Report from Hook: (%s) Reporter: %s Reported: %s", createdReport.reason, createdReport.reporter, createdReport.reported));
SpawnEvent.call(new ReportCreatedEvent(new ReportCreatedEvent.CreatedReport(createdReport.reporter, createdReport.reported, createdReport.reason)));
return HttpServer.nothing;
}));
record CreatedStrike(UUID uuid) {}
this.addAction("strike", new JsonAction<>(CreatedStrike.class, createdStrike -> {
Main.logger().info(String.format("New Strike from Hook! (User %s)", createdStrike.uuid));
SpawnEvent.call(new StrikeCreatedEvent(new StrikeCreatedEvent.CreatedStrike(createdStrike.uuid)));
return HttpServer.nothing;
}));
@@ -28,6 +28,7 @@ public abstract class Appliance {
@Retention(RetentionPolicy.RUNTIME)
public @interface Flags {
boolean enabled() default true;
boolean autoload() default true;
}
private String localConfigPath;
@@ -101,9 +102,20 @@ public abstract class Appliance {
}
public void destruct(@NotNull JavaPlugin plugin) {
if(this.listeners == null) return;
this.listeners.forEach(HandlerList::unregisterAll);
}
public void enableSequence(JavaPlugin plugin) {
this.onEnable();
this.initialize(plugin);
}
public void disableSequence(JavaPlugin plugin) {
this.onDisable();
this.destruct(plugin);
}
protected <T extends Appliance> T queryAppliance(Class<T> clazz) {
return Main.instance().getAppliance(clazz);
}
@@ -4,6 +4,8 @@ import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class ReportCreatedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@Override
@@ -15,7 +17,7 @@ public class ReportCreatedEvent extends Event {
return HANDLERS;
}
public record CreatedReport(String reporter, String reported, String reason) {}
public record CreatedReport(UUID reporter, UUID reported, String reason) {}
private final CreatedReport report;
public ReportCreatedEvent(CreatedReport report) {
@@ -1,10 +1,16 @@
package eu.mhsl.craftattack.spawn.core.util.entity;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class PlayerUtils {
public static void resetStatistics(Player player) {
for(Statistic statistic : Statistic.values()) {
@@ -30,4 +36,30 @@ public class PlayerUtils {
}
}
}
public static void sendCube(Player player, int cubeSize, BlockData fakeBlock) {
Location loc = player.getLocation();
World world = player.getWorld();
int half = cubeSize / 2;
int cx = loc.getBlockX();
int cy = loc.getBlockY();
int cz = loc.getBlockZ();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight() - 1;
Map<Location, BlockData> changes = new HashMap<>();
for (int x = cx - half; x <= cx + half; x++) {
for (int y = Math.max(cy - half, minY); y <= Math.min(cy + half, maxY); y++) {
for (int z = cz - half; z <= cz + half; z++) {
changes.put(new Location(world, x, y, z), fakeBlock);
}
}
}
//noinspection UnstableApiUsage
player.sendMultiBlockChange(changes);
}
}
@@ -5,23 +5,18 @@ 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.craftattack.appliances.gameplay.bloodmoon.commands.BloodmoonCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonEntityDamageListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonMonsterDeathListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonPlayerJoinListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.BloodmoonTimeListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener.*;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.*;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitTask;
@@ -33,44 +28,43 @@ import java.util.concurrent.ThreadLocalRandom;
@SuppressWarnings("FieldCanBeLocal")
public class Bloodmoon extends Appliance {
// für alle Dimensionen? einstellbar machen?
public final Map<EntityType, Set<MobEffect>> affectedMobEffectMap = Map.of(
EntityType.ZOMBIE, Set.of(
public final Map<EntityType, Set<MobEffect>> affectedMobEffectMap = Map.ofEntries(
Map.entry(EntityType.ZOMBIE, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.SKELETON, Set.of(
)),
Map.entry(EntityType.SKELETON, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 3.5, 1)
),
EntityType.SPIDER, Set.of(
)),
Map.entry(EntityType.SPIDER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.POISON, 4, 1),
new MobEffect.PotionMobEffect(PotionEffectType.NAUSEA, 6, 10)
),
EntityType.CREEPER, Set.of(
)),
Map.entry(EntityType.CREEPER, Set.of(
new MobEffect.LightningMobEffect()
),
EntityType.HUSK, Set.of(
)),
Map.entry(EntityType.HUSK, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.DROWNED, Set.of(
)),
Map.entry(EntityType.STRAY, Set.of()),
Map.entry(EntityType.DROWNED, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.WITCH, Set.of(),
EntityType.ZOMBIE_VILLAGER, Set.of(
)),
Map.entry(EntityType.WITCH, Set.of()),
Map.entry(EntityType.ZOMBIE_VILLAGER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
),
EntityType.PHANTOM, Set.of(
)),
Map.entry(EntityType.PHANTOM, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.LEVITATION, 1.5, 3)
),
EntityType.ENDERMAN, Set.of(
)),
Map.entry(EntityType.ENDERMAN, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 2.5, 2)
)
))
);
public final int expMultiplier = 3;
public final double mobDamageMultiplier = 2;
public final double mobHealthMultiplier = 2;
private boolean isActive = false;
private final ThreadLocalRandom random = ThreadLocalRandom.current();
private final BossBar bossBar = BossBar.bossBar(
Component.text("Blutmond", NamedTextColor.DARK_RED),
1f,
@@ -79,6 +73,7 @@ public class Bloodmoon extends Appliance {
Set.of(BossBar.Flag.CREATE_WORLD_FOG, BossBar.Flag.DARKEN_SCREEN)
);
private final boolean hordesEnabled = true;
public final boolean bloodmoonSkippable = true;
private final int hordeSpawnRateTicks = 40 * Ticks.TICKS_PER_SECOND;
private final int hordeSpawnRateVariationTicks = 40 * Ticks.TICKS_PER_SECOND;
private final int hordeMinPopulation = 3;
@@ -89,12 +84,13 @@ public class Bloodmoon extends Appliance {
EntityType.SKELETON,
EntityType.SPIDER
);
private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new HashMap<>();
private long lastBloodmoonStartTick = 0;
private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new WeakHashMap<>();
public final int ticksPerDay = 24000;
public final int bloodmoonLength = ticksPerDay/2;
public final int bloodmoonLength = this.ticksPerDay / 2;
public final int preStartMessageTicks = Ticks.TICKS_PER_SECOND * 50;
private boolean bossbarActive = false;
private final int bloodmoonFreeDaysAtStart = 3;
private final int bloodmoonStartTime = ticksPerDay/2;
private final int bloodmoonStartTime = this.ticksPerDay /2;
private final int bloodmoonDayInterval = 30;
private final int minBonusDrops = 1;
private final int maxBonusDrops = 4;
@@ -112,50 +108,93 @@ public class Bloodmoon extends Appliance {
}
public boolean bloodmoonIsActive() {
return this.isActive;
long currentTick = Bukkit.getWorlds().getFirst().getFullTime();
long day = currentTick / this.ticksPerDay;
if(day % this.bloodmoonDayInterval != 0 || day - this.bloodmoonFreeDaysAtStart <= 0) return false;
long time = currentTick % this.ticksPerDay;
return time >= this.bloodmoonStartTime && time <= this.bloodmoonStartTime + this.bloodmoonLength;
}
public void startBloodmoon(long startTick) {
this.lastBloodmoonStartTick = startTick;
this.isActive = true;
public void startBloodmoon() {
this.bossbarActive = true;
Bukkit.getOnlinePlayers().forEach(this::addPlayerToBossBar);
this.startHordeSpawning(this.getRandomHordeSpawnDelay());
this.sendStartMessages();
}
public void stopBloodmoon() {
this.isActive = false;
public void stopBloodmoonIfInactive() {
Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
if(!this.bossbarActive) return;
if(this.bloodmoonIsActive()) return;
this.bossbarActive = false;
Bukkit.getOnlinePlayers().forEach(player -> player.hideBossBar(this.bossBar));
this.stopHordeSpawning();
this.sendStopMessages();
},
1
);
}
// not working for entity effects and debuffs...
public void forceStopBloodmoon() {
this.bossbarActive = false;
Bukkit.getOnlinePlayers().forEach(player -> player.hideBossBar(this.bossBar));
this.stopHordeSpawning();
this.sendStopMessages();
}
public void updateBossBar() {
long tick = Bukkit.getWorlds().getFirst().getFullTime();
long sinceStart = tick - this.lastBloodmoonStartTick;
long sinceStart = tick - this.lastBloodmoonStartTick();
float progress = 1f - ((float) sinceStart / this.bloodmoonLength);
if(progress < 0) progress = 1f;
this.bossBar.progress(progress);
}
private long lastBloodmoonStartTick() {
long currentTick = Bukkit.getWorlds().getFirst().getFullTime();
long day = currentTick / this.ticksPerDay;
long time = currentTick % this.ticksPerDay;
boolean todayIsBloodmoon = (day % this.bloodmoonDayInterval == 0) && (day > this.bloodmoonFreeDaysAtStart);
if (todayIsBloodmoon && time < this.bloodmoonStartTime) {
day -= this.bloodmoonDayInterval;
} else if (!todayIsBloodmoon) {
day -= (day % this.bloodmoonDayInterval);
}
if (day <= this.bloodmoonFreeDaysAtStart) return -1L;
return day * this.ticksPerDay + this.bloodmoonStartTime;
}
public boolean isStartTick(long tick) {
long day = tick / this.ticksPerDay;
if(day % this.bloodmoonDayInterval != 0 || day - this.bloodmoonFreeDaysAtStart <= 0) return false;
long time = tick - (day * this.ticksPerDay);
if(day % this.bloodmoonDayInterval != 0 || day <= this.bloodmoonFreeDaysAtStart) return false;
long time = tick % this.ticksPerDay;
return time == this.bloodmoonStartTime;
}
private int getRandomHordeSpawnDelay() {
return this.hordeSpawnRateTicks + ThreadLocalRandom.current().nextInt(this.hordeSpawnRateVariationTicks);
return this.hordeSpawnRateTicks + this.random.nextInt(this.hordeSpawnRateVariationTicks);
}
private void startHordeSpawning(int delay) {
Bukkit.getOnlinePlayers().forEach(player -> this.startHordeSpawning(delay, player));
}
private void startHordeSpawning(int delay, Player player) {
private void stopHordeSpawning() {
this.hordeSpawnTasks.forEach((player, bukkitTask) -> {
@Nullable BukkitTask task = this.hordeSpawnTasks.get(player);
if(task != null) task.cancel();
});
}
public void startHordeSpawning(int delay, Player player) {
@Nullable BukkitTask task = this.hordeSpawnTasks.get(player);
if(task != null) task.cancel();
task = Bukkit.getScheduler().runTaskLater(
BukkitTask newTask = Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
if(!this.bloodmoonIsActive()) return;
@@ -164,7 +203,7 @@ public class Bloodmoon extends Appliance {
},
delay
);
this.hordeSpawnTasks.put(player, task);
this.hordeSpawnTasks.put(player, newTask);
}
public void addPlayerToBossBar(Player player) {
@@ -186,14 +225,17 @@ public class Bloodmoon extends Appliance {
public void spawnRandomHorde(Player player) {
if(!this.hordesEnabled) return;
if(!this.getBloodmoonSetting(player)) return;
if(!player.getGameMode().equals(GameMode.SURVIVAL)) return;
ThreadLocalRandom random = ThreadLocalRandom.current();
EntityType hordeEntityType = this.hordeMobList.get(random.nextInt(this.hordeMobList.size()));
int hordeSize = random.nextInt(this.hordeMinPopulation, this.hordeMaxPopulation + 1);
EntityType hordeEntityType = this.hordeMobList.get(this.random.nextInt(this.hordeMobList.size()));
if(player.getLocation().getWorld().getEnvironment().equals(World.Environment.THE_END)) hordeEntityType = EntityType.ENDERMITE;
int hordeSize = this.random.nextInt(this.hordeMinPopulation, this.hordeMaxPopulation + 1);
this.spawnHorde(player, hordeSize, hordeEntityType);
}
public void sendWarningMessage(Player p) {
if(!this.getBloodmoonSetting(p)) return;
p.sendMessage(Component.text("Der Blutmond waltet in diesem Augenblick!", NamedTextColor.RED));
}
@@ -207,20 +249,20 @@ public class Bloodmoon extends Appliance {
private void spawnHorde(Player player, int size, EntityType type) {
for(int i = 0; i < size; i++) {
double spawnRadiant = ThreadLocalRandom.current().nextDouble(0, 2*Math.PI);
double spawnRadiant = this.random.nextDouble(0, 2*Math.PI);
Location mobSpawnLocation = player.getLocation().add(
Math.sin(spawnRadiant)*this.hordeSpawnDistance,
0,
Math.cos(spawnRadiant)*this.hordeSpawnDistance
);
mobSpawnLocation.setY(player.getWorld().getHighestBlockYAt(mobSpawnLocation) + 1);
player.getWorld().spawnEntity(mobSpawnLocation, type);
player.getWorld().spawnEntity(mobSpawnLocation, type, CreatureSpawnEvent.SpawnReason.NATURAL);
player.getWorld().strikeLightningEffect(mobSpawnLocation);
}
}
public List<ItemStack> getRandomBonusDrops() {
int itemCount = ThreadLocalRandom.current().nextInt(this.minBonusDrops, this.maxBonusDrops + 1);
int itemCount = this.random.nextInt(this.minBonusDrops, this.maxBonusDrops + 1);
List<ItemStack> result = new ArrayList<>();
for(int i = 0; i < itemCount; i++) {
result.add(this.getRandomBonusDrop());
@@ -228,9 +270,9 @@ public class Bloodmoon extends Appliance {
return result;
}
private ItemStack getRandomBonusDrop() {
private @Nullable ItemStack getRandomBonusDrop() {
int totalWeight = this.bonusDropWeightMap.values().stream().mapToInt(value -> value).sum();
int randomInt = ThreadLocalRandom.current().nextInt(0, totalWeight + 1);
int randomInt = this.random.nextInt(0, totalWeight + 1);
int cumulativeWeight = 0;
for(Map.Entry<ItemStack, Integer> entry : this.bonusDropWeightMap.entrySet()) {
cumulativeWeight += entry.getValue();
@@ -268,7 +310,8 @@ public class Bloodmoon extends Appliance {
new BloodmoonMonsterDeathListener(),
new BloodmoonPlayerJoinListener(),
new BloodmoonEntityDamageListener(),
new BloodmoonTimeListener()
new BloodmoonTimeListener(),
new BloodmoonTimeSkipListener()
);
}
@@ -19,7 +19,7 @@ public class BloodmoonSetting extends BoolSetting implements CategorizedSetting
@Override
public SettingCategory category() {
return SettingCategory.Misc; // TODO: mehr als 8 bug fixen
return SettingCategory.Gameplay;
}
@Override
@@ -34,7 +34,7 @@ public class BloodmoonSetting extends BoolSetting implements CategorizedSetting
@Override
protected Material icon() {
return Material.CLOCK;
return Material.SKELETON_SKULL;
}
@Override
@@ -2,6 +2,7 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.comm
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
@@ -23,15 +24,19 @@ public class BloodmoonCommand extends ApplianceCommand<Bloodmoon> {
switch(args[0]) {
case "start": {
this.getAppliance().startBloodmoon(0L);
this.getAppliance().startBloodmoon();
sender.sendMessage("Started bloodmoon.");
break;
}
case "stop": {
this.getAppliance().stopBloodmoon();
this.getAppliance().forceStopBloodmoon();
sender.sendMessage("Stopped bloodmoon.");
break;
}
case "time": {
sender.sendMessage(String.valueOf(Bukkit.getWorlds().getFirst().getFullTime()));
break;
}
default: throw new Error("No such option: '%s' !".formatted(args[0]));
}
}
@@ -2,6 +2,7 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.list
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
@@ -9,7 +10,9 @@ public class BloodmoonPlayerJoinListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
this.getAppliance().addPlayerToBossBar(event.getPlayer());
this.getAppliance().sendWarningMessage(event.getPlayer());
Player player = event.getPlayer();
this.getAppliance().addPlayerToBossBar(player);
this.getAppliance().startHordeSpawning(1500, player);
this.getAppliance().sendWarningMessage(player);
}
}
@@ -3,6 +3,7 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.list
import com.destroystokyo.paper.event.server.ServerTickStartEvent;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
@@ -11,19 +12,19 @@ public class BloodmoonTimeListener extends ApplianceListener<Bloodmoon> {
public void onServerTick(ServerTickStartEvent event) {
long currentTime = Bukkit.getWorlds().getFirst().getFullTime();
if(this.getAppliance().isStartTick(currentTime)) {
this.getAppliance().startBloodmoon(currentTime);
this.getAppliance().startBloodmoon();
return;
}
if(this.getAppliance().isStartTick(currentTime - this.getAppliance().bloodmoonLength)) {
this.getAppliance().stopBloodmoon();
this.getAppliance().stopBloodmoonIfInactive();
return;
}
this.getAppliance().updateBossBar();
if(currentTime % Ticks.TICKS_PER_SECOND == 0 && this.getAppliance().bloodmoonIsActive()) this.getAppliance().updateBossBar();
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().ticksPerDay)) {
this.getAppliance().sendAnnouncementMessages();
return;
}
if(this.getAppliance().isStartTick(currentTime + 1000)) {
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().preStartMessageTicks)) {
this.getAppliance().sendPreStartMessages();
}
}
@@ -0,0 +1,29 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
import net.kyori.adventure.text.Component;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.world.TimeSkipEvent;
public class BloodmoonTimeSkipListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onTimeSkip(TimeSkipEvent event) {
if(this.getAppliance().bloodmoonSkippable || !event.getSkipReason().equals(TimeSkipEvent.SkipReason.NIGHT_SKIP)) {
this.getAppliance().stopBloodmoonIfInactive();
return;
}
event.setCancelled(true);
}
@EventHandler
public void onPlayerSleep(PlayerBedEnterEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
if(this.getAppliance().bloodmoonSkippable) return;
if(!event.getBedEnterResult().equals(PlayerBedEnterEvent.BedEnterResult.OK)) return;
event.setUseBed(Event.Result.DENY);
event.getPlayer().sendActionBar(Component.text("Du kannst während dem Blutmond nicht schlafen"));
}
}
@@ -39,7 +39,6 @@ public class CustomAdvancements extends Appliance {
player.getAdvancementProgress(advancement).awardCriteria("criteria");
} catch(Exception e) {
Main.logger().info("Advancement " + advancementName + " not found! (is Custom Advancements data pack loaded?)");
throw e;
}
}
@@ -0,0 +1,21 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.mobSilenceNametag;
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 FirstTestClass extends Appliance {
public String greetPlayer(Player player) {
return "Hello, " + player.getName() + "! Test 2";
}
protected @NotNull List<Listener> listeners() {
return List.of(new FirstTestListener());
}
}
@@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.mobSilenceNametag;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
public class FirstTestListener extends ApplianceListener<FirstTestClass> {
@EventHandler
public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) {
String greeting = this.getAppliance().greetPlayer(event.getPlayer());
event.getPlayer().sendMessage(greeting);
}
}
@@ -51,14 +51,15 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
void askForConfirmation(Player player) {
Component confirmationMessage = switch(this.getLawStatus(player)) {
case DISABLED -> Component.text("Wenn du Vogelfrei aktivierst, darfst du von allen anderen vogelfreien Spielern grundlos angegriffen werden.");
case VOLUNTARILY -> Component.text("Wenn du Vogelfrei deaktivierst, darfst du nicht mehr grundlos von anderen Spielern angegriffen werden.");
case FORCED -> Component.text("Du darfst zurzeit deinen Vogelfreistatus nicht ändern, da dieser als Strafe auferlegt wurde!");
case DISABLED -> Component.text("Wenn du den PVP-Modus aktivierst, darfst du von allen anderen PVP-Spielern grundlos angegriffen werden." +
" Du kannst den PVP-Modus erst wieder nach " + this.timeoutInMs / 1000 / 60 / 60 + " Stunden deaktivieren!");
case VOLUNTARILY -> Component.text("Wenn du den PVP-Modus deaktivierst, darfst du nicht mehr grundlos von anderen Spielern angegriffen werden.");
case FORCED -> Component.text("Du darfst zurzeit deinen PVP-Modus nicht ändern, da die˝ser als Strafe auferlegt wurde!");
};
String command = String.format("/%s confirm", OutlawedCommand.commandName);
Component changeText = Component.text(
String.format(
"Zum ändern deines Vogelfrei status klicke auf diese Nachricht oder tippe '%s'",
"Zum ändern deines PVP-Status klicke auf diese Nachricht oder tippe '%s'",
command
),
NamedTextColor.GOLD
@@ -74,11 +75,11 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
void switchLawStatus(Player player) throws OutlawChangeNotPermitted {
if(this.getLawStatus(player).equals(Status.FORCED)) {
throw new OutlawChangeNotPermitted("Dein Vogelfreistatus wurde als Strafe auferlegt und kann daher nicht verändert werden.");
throw new OutlawChangeNotPermitted("Dein PVP-Status wurde als Strafe auferlegt und kann daher nicht verändert werden.");
}
if(this.isTimeout(player)) {
throw new OutlawChangeNotPermitted("Du kannst deinen Vogelfreistatus nicht so schnell wechseln. Bitte warte einige Stunden bevor du umschaltest!");
throw new OutlawChangeNotPermitted("Du kannst deinen PVP-Status nicht so schnell wechseln. Bitte warte einige Stunden bevor du umschaltest!");
}
this.setLawStatus(player, this.isOutlawed(player) ? Status.DISABLED : Status.VOLUNTARILY);
@@ -126,13 +127,13 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
public Component getStatusDescription(Status status) {
return switch(status) {
case DISABLED -> Component.text("Vogelfreistatus inaktiv: ", NamedTextColor.GREEN)
case DISABLED -> Component.text("PVP-Modus inaktiv: ", NamedTextColor.GREEN)
.append(Component.text("Es gelten die normalen Regeln!", NamedTextColor.GOLD));
case VOLUNTARILY, FORCED -> Component.text("Vogelfreistatus aktiv: ", NamedTextColor.RED)
case VOLUNTARILY, FORCED -> Component.text("PVP-Modus aktiv: ", NamedTextColor.RED)
.append(Component.text(
"Du darfst von allen anderen vogelfreien Spielern angegriffen und getötet werden!" +
"Wenn du getötet wirst, müssen andere Spieler deine Items nicht zurückerstatten!",
"Du darfst von allen anderen PVP-Spielern angegriffen und getötet werden!\n" +
"Wenn du getötet wirst, müssen andere Spieler deine Items *nicht* zurückerstatten!",
NamedTextColor.GOLD
));
};
@@ -143,7 +144,7 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
if(this.isOutlawed(player)) {
return Component.text("[☠]", NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(
"Vogelfreie Spieler dürfen von anderen vogelfreien Spielern ohne Grund angegriffen werden!"
"PVP-Modus Spieler dürfen von anderen vogelfreien Spielern ohne Grund angegriffen werden!"
)));
}
@@ -8,7 +8,7 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class OutlawedCommand extends ApplianceCommand.PlayerChecked<Outlawed> {
public static final String commandName = "vogelfrei";
public static final String commandName = "pvp";
public OutlawedCommand() {
super(commandName);
@@ -0,0 +1,20 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.portableCrafting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
class OnCraftingBlockUseListener extends ApplianceListener<PortableCrafting> {
@EventHandler
public void inInteract(PlayerInteractEvent event) {
if(!event.getAction().equals(Action.RIGHT_CLICK_AIR)) return;
if(!Settings.instance().getSetting(event.getPlayer(), Settings.Key.EnablePortableCrafting, Boolean.class)) return;
switch(event.getMaterial()) {
case CRAFTING_TABLE -> this.getAppliance().openCraftingTable(event.getPlayer());
case STONECUTTER -> this.getAppliance().openStonecutter(event.getPlayer());
}
}
}
@@ -1,16 +0,0 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.portableCrafting;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
class OnCraftingTableUseListener extends ApplianceListener<PortableCrafting> {
@EventHandler
public void inInteract(PlayerInteractEvent event) {
if(!event.getAction().equals(Action.RIGHT_CLICK_AIR)) return;
if(!event.getMaterial().equals(Material.CRAFTING_TABLE)) return;
this.getAppliance().openFor(event.getPlayer());
}
}
@@ -14,13 +14,16 @@ public class PortableCrafting extends Appliance {
Settings.instance().declareSetting(PortableCraftingSetting.class);
}
public void openFor(Player player) {
if(!Settings.instance().getSetting(player, Settings.Key.EnablePortableCrafting, Boolean.class)) return;
public void openCraftingTable(Player player) {
player.openWorkbench(null, true);
}
public void openStonecutter(Player player) {
player.openStonecutter(null, true);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new OnCraftingTableUseListener());
return List.of(new OnCraftingBlockUseListener());
}
}
@@ -18,7 +18,7 @@ public class PortableCraftingSetting extends BoolSetting implements CategorizedS
@Override
protected String description() {
return "Erlaubt das öffnen einer Werkbank in der Hand, ohne sie plazieren zu müssen";
return "Erlaubt das öffnen einer Werkbank oder einer Steinsäge in der Hand, ohne den Block plazieren zu müssen";
}
@Override
@@ -2,6 +2,7 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
import eu.mhsl.craftattack.spawn.craftattack.api.repositories.EventRepository;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
@@ -26,6 +27,7 @@ import org.bukkit.entity.Villager;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.geysermc.cumulus.form.SimpleForm;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@@ -37,6 +39,11 @@ public class Event extends Appliance {
DONE
}
public enum EventType {
BIG,
SMALL
}
Countdown advertiseCountdown = new Countdown(
120,
announcementData -> Component.text()
@@ -50,6 +57,7 @@ public class Event extends Appliance {
);
public DisplayVillager.ConfigBound villager;
private boolean isOpen = false;
private EventType eventType;
private AdvertisementStatus advertiseStatus = AdvertisementStatus.BEFORE;
private UUID roomId;
private final List<Reward> pendingRewards = new ArrayList<>();
@@ -79,21 +87,31 @@ public class Event extends Appliance {
if(this.isOpen) this.roomId = UUID.fromString(this.localConfig().getString("roomId", ""));
}
public void openEvent() {
public void openEvent(EventType type) {
if(this.isOpen) throw new ApplianceCommand.Error("Es läuft derzeit bereits ein Event!");
this.eventType = type;
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.CreatedRoom> sessionResponse = this.queryRepository(EventRepository.class).createSession();
if(type.equals(EventType.SMALL)) {
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.CreatedRoom> sessionResponse = this.queryRepository(EventRepository.class).createSession();
if(sessionResponse.status() != HttpStatus.OK)
throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status());
if(sessionResponse.status() != HttpStatus.OK)
throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status());
this.isOpen = true;
this.roomId = sessionResponse.data().uuid();
});
this.isOpen = true;
this.roomId = sessionResponse.data().uuid();
});
return;
}
this.isOpen = true;
}
public void joinEvent(Player p) {
this.joinEvent(p, false);
}
public void joinEvent(Player p, boolean ignoreBedrock) {
if(!this.isOpen) {
p.sendMessage(Component.text("Zurzeit ist kein Event geöffnet.", NamedTextColor.RED));
return;
@@ -109,21 +127,43 @@ public class Event extends Appliance {
return;
}
if(!ignoreBedrock && Floodgate.isBedrock(p)) {
Floodgate.getBedrockPlayer(p).sendForm(
SimpleForm.builder()
.title("Achtung!")
.content("Je nach deiner Minecraft-Bedrock-Version kann dein Minecraft in den Events abstürzen. " +
"Ggf. ist also für dich ein Mitspielen auf der Bedrock-Edition nicht möglich.")
.button("Ok, lass es uns versuchen")
.button("Abbrechen")
.validResultHandler(simpleFormResponse -> {
if(simpleFormResponse.clickedButtonId() != 0) return;
this.joinEvent(p, true);
})
.build()
);
return;
}
Main.instance().getLogger().info("Verbinde mit eventserver: " + p.getName());
p.sendMessage(Component.text("Authentifiziere...", NamedTextColor.GREEN));
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.QueueRoom.Response> queueResponse = this.queryRepository(EventRepository.class)
.queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId));
if(this.eventType.equals(EventType.SMALL)) {
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.QueueRoom.Response> queueResponse = this.queryRepository(EventRepository.class)
.queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId));
if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) {
p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED));
return;
}
if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) {
p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED));
return;
}
p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN));
PluginMessage.connect(p, this.localConfig().getString("connect-server-name"));
});
p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN));
PluginMessage.connect(p, this.localConfig().getString("connect-server-name"));
});
return;
}
PluginMessage.connect(p, "grand-event");
}
public void endEvent() {
@@ -7,6 +7,10 @@ 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.Arrays;
import java.util.List;
public class EventOpenSessionCommand extends ApplianceCommand<Event> {
public EventOpenSessionCommand() {
@@ -15,7 +19,17 @@ public class EventOpenSessionCommand extends ApplianceCommand<Event> {
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().openEvent();
if(args.length == 0) {
this.getAppliance().openEvent(Event.EventType.SMALL);
} else {
this.getAppliance().openEvent(Event.EventType.valueOf(args[0]));
}
sender.sendMessage(Component.text("Event-Server gestartet!", NamedTextColor.GREEN));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 1) return Arrays.stream(Event.EventType.values()).map(Enum::toString).toList();
return null;
}
}
@@ -9,7 +9,6 @@ import eu.mhsl.craftattack.spawn.core.api.HttpStatus;
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.entity.Entity;
import org.bukkit.entity.Player;
@@ -52,7 +51,7 @@ public class Feedback extends Appliance {
message
.append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN)
.hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(feedbackUrl))))
.append(ComponentUtil.clickLink(feedbackUrl)))
.appendNewline()
.append(border);
@@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.List;
@Appliance.Flags(autoload = false, enabled = false)
public class WorldMuseum extends Appliance {
public DisplayVillager.ConfigBound villager;
@@ -22,6 +22,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -29,14 +30,14 @@ import java.util.Map;
import java.util.Objects;
import static java.util.Map.entry;
import static org.bukkit.Sound.MUSIC_DISC_PRECIPICE;
import static org.bukkit.Sound.MUSIC_DISC_LAVA_CHICKEN;
public class ProjectStart extends Appliance {
private final int startMusicAt = 293;
private final int startMusicAt = 130;
private final World startWorld = Bukkit.getWorld("world");
private final Location glassLocation = new Location(this.startWorld, 0, 64, -300);
private final Location glassLocation = new Location(this.startWorld, -363, 126, 613);
private final List<Location> netherFireLocations = List.of(
new Location(this.startWorld, 14, 71, -310)
new Location(this.startWorld, -352, 131, 627)
);
private final Countdown countdown = new Countdown(
@@ -47,7 +48,7 @@ public class ProjectStart extends Appliance {
);
private final BlockCycle blockCycle = new BlockCycle(
this.glassLocation,
Material.RED_STAINED_GLASS,
Material.WHITE_STAINED_GLASS,
List.of(
Material.RED_STAINED_GLASS,
Material.YELLOW_STAINED_GLASS,
@@ -77,7 +78,7 @@ public class ProjectStart extends Appliance {
counter -> counter == this.startMusicAt,
counter -> this.glassLocation
.getWorld()
.playSound(this.glassLocation, MUSIC_DISC_PRECIPICE, SoundCategory.RECORDS, 500f, 1f)
.playSound(this.glassLocation, MUSIC_DISC_LAVA_CHICKEN, SoundCategory.RECORDS, 500f, 1f)
)
);
}
@@ -121,14 +122,20 @@ public class ProjectStart extends Appliance {
IteratorUtil.worlds(World::getWorldBorder, worldBorder -> worldBorder.setSize(worldBorder.getMaxSize()));
IteratorUtil.worlds(world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, false));
IteratorUtil.worlds(world -> world.setFullTime(0));
Bukkit.getWorlds().getFirst().setFullTime(0);
this.netherFireLocations.forEach(location -> Objects.requireNonNull(this.startWorld).getBlockAt(location).setType(Material.FIRE));
Bukkit.getOnlinePlayers().forEach(player -> {
player.setFoodLevel(20);
player.setHealth(20);
player.getInventory().clear();
if(player.getInventory().contains(Material.RECOVERY_COMPASS)) {
player.getInventory().clear();
player.getInventory().addItem(new ItemStack(Material.RECOVERY_COMPASS));
} else {
player.getInventory().clear();
}
player.setGameMode(GameMode.SURVIVAL);
player.setExp(0);
player.setLevel(0);
@@ -20,9 +20,10 @@ public class Strikes extends Appliance {
private final Map<Integer, Duration> strikePunishmentMap = Map.of(
1, Duration.ofHours(1),
2, Duration.ofHours(24),
3, Duration.ofDays(3),
4, Duration.ofDays(7)
2, Duration.ofHours(3),
3, Duration.ofDays(1),
4, Duration.ofDays(2),
5, Duration.ofDays(3)
);
@@ -49,7 +49,7 @@ public class Whitelist extends Appliance {
? Floodgate.getBedrockPlayer(player).getUsername()
: player.getName();
if(!user.username().trim().equalsIgnoreCase(purePlayerName))
if(!user.username().trim().equalsIgnoreCase(purePlayerName) && !Floodgate.isBedrock(player)) // TODO: Bedrock Namen mit leerzeichen funktionieren nicht, daher die ausnahme bei der NUtzernamenprüfung
throw new DisconnectInfo.Throwable(
"Nutzername geändert",
String.format("Der Name '%s' stimmt nicht mit '%s' überein.", user.username(), player.getName()),
@@ -12,6 +12,6 @@ class InfectionSpawnListener extends ApplianceListener<ArmadilloInfectionReducer
public void onSpawn(CreatureSpawnEvent event) {
if(!event.getSpawnReason().equals(CreatureSpawnEvent.SpawnReason.POTION_EFFECT)) return;
if(!event.getEntity().getType().equals(EntityType.SILVERFISH)) return;
if(ThreadLocalRandom.current().nextDouble() > 0.7) event.setCancelled(true);
if(ThreadLocalRandom.current().nextDouble() > 0.8) event.setCancelled(true);
}
}
@@ -1,10 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tweaks.silverfishExpReducer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDeathEvent;
@Appliance.Flags(enabled = false)
class SilverfishDeathListener extends ApplianceListener<SilverfishExpReducer> {
@EventHandler
public void onDeath(EntityDeathEvent event) {
+9
View File
@@ -0,0 +1,9 @@
dependencies {
implementation project(':common')
implementation project(':core')
compileOnly 'io.papermc.paper:paper-api:1.21.10-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'
}
@@ -0,0 +1,162 @@
package eu.mhsl.craftattack.spawn.event.appliances.deathrun;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners.DeathrunPlayerDamageListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners.DeathrunPlayerJoinListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners.DeathrunPlayerMoveListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners.DeathrunPortalListener;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.Event;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.Scorable;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard.EventScoreboardBuilder;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Appliance.Flags(autoload = false)
public class Deathrun extends Appliance implements Event, Scorable {
private final EventScoreboardBuilder scoreboardBuilder = new EventScoreboardBuilder(this, 3, 2, 0);
private final double borderDistance = 100;
private final int borderVisibilityDistance = 8;
private long durationSeconds;
private boolean isBeforeStart = true;
private boolean pvpDisabled = true;
private final World world = Bukkit.getWorlds().getFirst();
private BukkitTask pvpTask;
public ArrayList<UUID> previouslyJoinedPlayers = new ArrayList<>();
@Override
public void onEnable() {
this.world.getWorldBorder().setCenter(this.world.getSpawnLocation());
this.world.getWorldBorder().setSize(20);
Bukkit.getOnlinePlayers().forEach(player -> {
player.teleport(this.world.getSpawnLocation());
this.previouslyJoinedPlayers.add(player.getUniqueId());
});
this.pvpDisabled = true;
}
public double getBorderDistance() {
return this.borderDistance;
}
public int getBorderVisibilityDistance() {
return this.borderVisibilityDistance;
}
public void spawnParticles(Player p, double xMin, double xMax, double yMin, double yMax, double zMin, double zMax) {
Particle particle = Particle.WAX_ON;
for (double y = yMin; y <= yMax; y += 0.5) {
for (double z = zMin; z <= zMax; z += 0.5) {
for (double x = xMin; x <= xMax; x += 0.5) {
p.spawnParticle(particle, x, y, z, 1, 0.05, 0.05, 0.05, 0);
}
}
}
}
@Override
public void onDisable() {
this.world.getWorldBorder().setSize(this.world.getWorldBorder().getMaxSize());
}
@Override
public void start(long durationSeconds) {
this.isBeforeStart = false;
this.durationSeconds = durationSeconds;
Title title = Title.title(Component.text("Start", NamedTextColor.GOLD), Component.text("Laufe Richtung Osten! (positiv x)", NamedTextColor.YELLOW));
// TODO: Soll PvP überhaupt aktiviert werden? Soll Respawn erlaubt sein?
Bukkit.getOnlinePlayers().forEach(player -> {
player.showTitle(title);
player.sendMessage(Component.text("Start! Laufe Richtung Osten (positiv x)!", NamedTextColor.YELLOW));
});
Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> Bukkit.getOnlinePlayers().forEach(player ->
player.sendMessage(Component.text("PvP wird in 10 Minuten aktiviert!", NamedTextColor.GOLD))
),
Ticks.TICKS_PER_SECOND * 5
);
this.world.getWorldBorder().setSize(this.world.getWorldBorder().getMaxSize());
this.pvpTask = Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
this.pvpDisabled = false;
Bukkit.getOnlinePlayers().forEach(player -> {
player.sendMessage(Component.text("PvP ist jetzt aktiviert!", NamedTextColor.GOLD));
player.playSound(Sound.sound(org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.MASTER, 500f, 2f));
});
},
Ticks.TICKS_PER_SECOND * 60 * 10
);
}
public boolean isBeforeStart() {
return this.isBeforeStart;
}
public World getWorld() {
return this.world;
}
public boolean isPvpDisabled() {
return this.pvpDisabled;
}
@Override
public int getScore(Player p) {
return (int) Math.ceil(p.getLocation().x());
}
@Override
public void stop() {
this.getScoreboardBuilder().stopAutomaticUpdates();
Title title = Title.title(Component.text("Ende!"), Component.empty());
if(this.pvpTask != null) this.pvpTask.cancel();
this.pvpDisabled = true;
Bukkit.getOnlinePlayers().forEach(player -> {
player.showTitle(title);
player.setScoreboard(Bukkit.getScoreboardManager().getNewScoreboard());
});
}
@Override
public long getDurationSeconds() {
return this.durationSeconds;
}
@Override
public String getScoreboardName() {
return "Deathrun";
}
@Override
public EventScoreboardBuilder getScoreboardBuilder() {
return this.scoreboardBuilder;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new DeathrunPlayerMoveListener(),
new DeathrunPlayerDamageListener(),
new DeathrunPlayerJoinListener(),
new DeathrunPortalListener()
);
}
}
@@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.Deathrun;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
public class DeathrunPlayerDamageListener extends ApplianceListener<Deathrun> {
@EventHandler
public void onPlayerDamagePlayer(EntityDamageByEntityEvent event) {
if(!this.getAppliance().isPvpDisabled()) return;
if(!(event.getDamager() instanceof Player)) return;
if(!(event.getEntity() instanceof Player)) return;
event.setCancelled(true);
}
@EventHandler
public void onPlayerDamage(EntityDamageEvent event) {
if(!this.getAppliance().isBeforeStart()) return;
if(!(event.getEntity() instanceof Player)) return;
event.setCancelled(true);
}
}
@@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.Deathrun;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class DeathrunPlayerJoinListener extends ApplianceListener<Deathrun> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if(
this.getAppliance().isBeforeStart() ||
!this.getAppliance().previouslyJoinedPlayers.contains(player.getUniqueId())
) {
player.teleport(this.getAppliance().getWorld().getSpawnLocation());
player.setGameMode(GameMode.ADVENTURE);
this.getAppliance().previouslyJoinedPlayers.add(player.getUniqueId());
}
if(!this.getAppliance().isBeforeStart() && player.getGameMode().equals(GameMode.ADVENTURE)) {
player.setGameMode(GameMode.SURVIVAL);
}
}
}
@@ -0,0 +1,49 @@
package eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.Deathrun;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
public class DeathrunPlayerMoveListener extends ApplianceListener<Deathrun> {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
Location spawnLocation = Bukkit.getWorlds().getFirst().getSpawnLocation();
double minX = spawnLocation.x() - this.getAppliance().getBorderDistance();
double minZ = spawnLocation.z() - this.getAppliance().getBorderDistance();
double maxZ = spawnLocation.z() + this.getAppliance().getBorderDistance();
if(event.getTo().x() < minX + this.getAppliance().getBorderVisibilityDistance()) {
this.getAppliance().spawnParticles(event.getPlayer(), minX-0.2, minX-0.2, event.getTo().y()-0.5, event.getTo().y()+2.5, event.getTo().z()-1.5, event.getTo().z()+1.5);
if(event.getTo().x() < minX) {
event.setTo(event.getTo().clone().set(minX, event.getTo().y(), event.getTo().z()));
}
}
if(event.getTo().z() < minZ + this.getAppliance().getBorderVisibilityDistance()) {
this.getAppliance().spawnParticles(event.getPlayer(), event.getTo().x()-1.5, event.getTo().x()+1.5, event.getTo().y()-0.5, event.getTo().y()+2.5, minZ-0.2, minZ-0.2);
if(event.getTo().z() < minZ) {
event.setTo(event.getTo().clone().set(event.getTo().x(), event.getTo().y(), minZ));
}
}
if(event.getTo().z() > maxZ - this.getAppliance().getBorderVisibilityDistance()) {
this.getAppliance().spawnParticles(event.getPlayer(), event.getTo().x()-1.5, event.getTo().x()+1.5, event.getTo().y()-0.5, event.getTo().y()+2.5, maxZ+0.2, maxZ+0.2);
if(event.getTo().z() > maxZ) {
event.setTo(event.getTo().clone().set(event.getTo().x(), event.getTo().y(), maxZ));
}
}
}
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) throws Exception {
Location spawnLocation = Bukkit.getWorlds().getFirst().getSpawnLocation();
double minX = spawnLocation.x() - this.getAppliance().getBorderDistance();
double minZ = spawnLocation.z() - this.getAppliance().getBorderDistance();
double maxZ = spawnLocation.z() + this.getAppliance().getBorderDistance();
if(event.getTo().x() < minX || event.getTo().z() < minZ || event.getTo().z() > maxZ) {
event.setCancelled(true);
throw new Exception("Player %s teleported outside the border.".formatted(event.getPlayer()));
}
}
}
@@ -0,0 +1,13 @@
package eu.mhsl.craftattack.spawn.event.appliances.deathrun.listeners;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.event.appliances.deathrun.Deathrun;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerPortalEvent;
public class DeathrunPortalListener extends ApplianceListener<Deathrun> {
@EventHandler
public void onPlayerPortal(PlayerPortalEvent event) {
event.setCancelled(true);
}
}
@@ -0,0 +1,7 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController;
public interface Event {
void start(long durationSeconds);
void stop();
long getDurationSeconds();
}
@@ -0,0 +1,206 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController;
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.core.util.IteratorUtil;
import eu.mhsl.craftattack.spawn.core.util.entity.PlayerUtils;
import eu.mhsl.craftattack.spawn.core.util.server.PluginMessage;
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.spawn.core.util.text.Countdown;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.commands.BigEventCommand;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.commands.JoinCraftattackCommand;
import net.kyori.adventure.sound.Sound;
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.GameMode;
import org.bukkit.GameRule;
import org.bukkit.Statistic;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
public class EventController extends Appliance {
private List<Appliance> eventAppliances = null;
private Event selectedEvent = null;
private int timerTaskId = -1;
private long durationMinutes;
private final Countdown countdown = new Countdown(
10,
this::format,
this::announce,
this::startEvent
);
private final Map<GameRule<Boolean>, Boolean> gameRulesAfterStart = Map.ofEntries(
entry(GameRule.DO_DAYLIGHT_CYCLE, true),
entry(GameRule.DO_INSOMNIA, true),
entry(GameRule.DISABLE_RAIDS, false),
entry(GameRule.DO_FIRE_TICK, true),
entry(GameRule.DO_ENTITY_DROPS, true),
entry(GameRule.DO_PATROL_SPAWNING, true),
entry(GameRule.DO_TRADER_SPAWNING, true),
entry(GameRule.DO_WEATHER_CYCLE, true),
entry(GameRule.FALL_DAMAGE, true),
entry(GameRule.FIRE_DAMAGE, true)
);
@Override
public void onEnable() {
this.eventAppliances = Main.instance().getAppliances().stream()
.filter(appliance -> appliance instanceof Event)
.toList();
}
public List<Appliance> getEventAppliances() {
if(this.eventAppliances == null) throw new IllegalStateException("Event appliances were not initialized");
return this.eventAppliances;
}
public void loadEvent(Appliance appliance) {
if(!(appliance instanceof Event)) throw new IllegalArgumentException("Appliance has to implement Event.");
this.unloadEvent();
Appliance newAppliance = Main.instance().restartAppliance(appliance.getClass());
if(!(newAppliance instanceof Event newEvent)) throw new IllegalArgumentException("Appliance has to implement Event.");
this.selectedEvent = newEvent;
Bukkit.getOnlinePlayers().forEach(player -> player.setGameMode(GameMode.ADVENTURE));
IteratorUtil.worlds(world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, true));
}
public void unloadEvent() {
if(this.selectedEvent == null) return;
this.selectedEvent.stop();
if(this.selectedEvent instanceof Appliance appliance) {
appliance.disableSequence(Main.instance());
}
this.selectedEvent = null;
}
public boolean hasLoadedEvent() {
return this.selectedEvent != null;
}
public void scheduleStart(long durationMinutes) {
if(!this.hasLoadedEvent()) throw new ApplianceCommand.Error("There is no event selected!");
this.durationMinutes = durationMinutes;
this.countdown.start();
}
public void cancelStart() {
if(this.countdown.isRunning()) this.countdown.cancel();
}
private Component format(Countdown.AnnouncementData data) {
return Component.text()
.append(ComponentUtil.createRainbowText(this.selectedEvent.getClass().getSimpleName(), 10))
.append(Component.text(" startet in ", NamedTextColor.GOLD))
.append(Component.text(data.count(), NamedTextColor.AQUA))
.append(Component.text(" " + data.unit() + "!", NamedTextColor.GOLD))
.build();
}
private void announce(Component message) {
IteratorUtil.onlinePlayers(player -> {
player.sendMessage(message);
player.playSound(Sound.sound(org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.MASTER, 500f, 1f));
});
}
private void startEvent() {
this.startEvent(this.durationMinutes);
}
public void startEvent(long durationMinutes) {
if(this.selectedEvent == null) throw new ApplianceCommand.Error("There is no event selected!");
IteratorUtil.worlds(world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, false));
IteratorUtil.worlds(world -> world.setFullTime(0));
Bukkit.getOnlinePlayers().forEach(player -> {
player.setFoodLevel(20);
player.setHealth(20);
player.getInventory().clear();
player.setGameMode(GameMode.SURVIVAL);
player.setExp(0);
player.setLevel(0);
player.playSound(Sound.sound(org.bukkit.Sound.ITEM_GOAT_HORN_SOUND_6, Sound.Source.MASTER, 500f, 1f));
player.sendMessage(Component.text("Viel Spaß bei %s!".formatted(this.selectedEvent.getClass().getSimpleName()), NamedTextColor.GREEN));
player.setStatistic(Statistic.TIME_SINCE_REST, 0);
PlayerUtils.resetStatistics(player);
});
this.selectedEvent.start(durationMinutes * 60);
if(this.selectedEvent instanceof Scorable scorable && scorable.automaticUpdates()) scorable.getScoreboardBuilder().startAutomaticUpdates();
// TODO: possibility for other dimensions
this.timerTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(
Main.instance(),
this::updateTimer,
Ticks.TICKS_PER_SECOND,
Ticks.TICKS_PER_SECOND
);
}
public void stopEvent() {
if(this.selectedEvent == null) throw new ApplianceCommand.Error("There is no event selected!");
this.selectedEvent.stop();
if(this.timerTaskId != -1) Bukkit.getScheduler().cancelTask(this.timerTaskId);
this.timerTaskId = -1;
if(this.selectedEvent instanceof Scorable scorable) {
String scores = String.join("\n", scorable.getScoreboardBuilder().getScores());
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(scores));
Main.instance().getLogger().info(scores);
}
Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
Bukkit.getOnlinePlayers().forEach(player -> PluginMessage.connect(player, "craftattack"));
this.unloadEvent();
},
Ticks.TICKS_PER_SECOND * 7
);
}
public String getSelectedEvent() {
if(this.selectedEvent == null) return "nothing selected";
return this.selectedEvent.getClass().getSimpleName().toLowerCase();
}
private void updateTimer() {
long ticksLeft = -(Bukkit.getWorlds().getFirst().getFullTime() - this.selectedEvent.getDurationSeconds() * Ticks.TICKS_PER_SECOND);
if(ticksLeft <= 0) {
Bukkit.getOnlinePlayers().forEach(player -> player.sendActionBar(Component.text("Fertig!")));
this.stopEvent();
return;
}
Bukkit.getOnlinePlayers().forEach(player -> player.sendActionBar(
Component.text(formatSeconds(ticksLeft / Ticks.TICKS_PER_SECOND), NamedTextColor.GOLD)
));
}
public static String formatSeconds(long seconds){
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new EventPlayerLoginListener()
);
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new BigEventCommand(),
new JoinCraftattackCommand()
);
}
}
@@ -0,0 +1,15 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import net.kyori.adventure.text.Component;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class EventPlayerLoginListener extends ApplianceListener<EventController> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if(event.getPlayer().isOp()) return;
if(this.getAppliance().hasLoadedEvent()) return;
event.getPlayer().kick(Component.text("Es ist kein Event geladen."));
}
}
@@ -0,0 +1,13 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard.EventScoreboardBuilder;
import org.bukkit.entity.Player;
public interface Scorable {
String getScoreboardName();
default boolean automaticUpdates() {
return true;
}
EventScoreboardBuilder getScoreboardBuilder();
int getScore(Player p);
}
@@ -0,0 +1,76 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController.commands;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.EventController;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class BigEventCommand extends ApplianceCommand<EventController> {
public BigEventCommand() {
super("event");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 0) throw new Error("No argument selected!");
switch(args[0]) {
case "unload": {
this.getAppliance().unloadEvent();
break;
}
case "load": {
if(args.length == 1) throw new Error("Not enough arguments for select.");
this.getAppliance().loadEvent(this.findApplianceFromString(args[1]));
break;
}
case "selected": {
sender.sendMessage("This Event is currently selected: %s".formatted(this.getAppliance().getSelectedEvent()));
break;
}
case "start": {
if(args.length == 1) {
this.getAppliance().scheduleStart(60 * 2);
break;
}
if(args.length == 2) {
try {
this.getAppliance().scheduleStart(Long.parseLong(args[1]));
} catch(NumberFormatException e) {
throw new Error("Last argument has to be a long.");
}
}
break;
}
case "stop": {
this.getAppliance().cancelStart();
this.getAppliance().stopEvent();
break;
}
default: throw new Error("No such option: '%s' !".formatted(args[0]));
}
}
private Appliance findApplianceFromString(String name) {
System.out.println(this.getAppliance().getEventAppliances());
return this.getAppliance().getEventAppliances().stream()
.filter(appliance -> appliance.getClass().getSimpleName().equalsIgnoreCase(name))
.findFirst()
.orElseThrow(() -> new Error("Event appliance '%s' not found.".formatted(name)));
}
@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("load", "unload", "selected", "start", "stop");
if(args.length == 2 && args[0].equals("load")) return this.getAppliance().getEventAppliances().stream()
.map(appliance -> appliance.getClass().getSimpleName().toLowerCase())
.toList();
if(args.length == 2 && args[0].equals("start")) return List.of("<minutes>");
return null;
}
}
@@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController.commands;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.server.PluginMessage;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.EventController;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class JoinCraftattackCommand extends ApplianceCommand.PlayerChecked<EventController> {
public JoinCraftattackCommand() {
super("craftattack");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
PluginMessage.connect(this.getPlayer(), "craftattack");
}
}
@@ -0,0 +1,6 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard;
import java.util.UUID;
public record EventScoreEntry(UUID playerUuid, String name, int score) {
}
@@ -0,0 +1,185 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController.scoreboard;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.event.appliances.eventController.Scorable;
import io.papermc.paper.scoreboard.numbers.NumberFormat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import java.util.*;
import java.util.stream.IntStream;
public class EventScoreboardBuilder {
private final int topPlaces;
private final int aroundPlaces;
private final int bottomPlaces;
private final Scorable scorable;
private final Comparator<EventScoreEntry> scoreComparator;
private final List<EventScoreEntry> playerScores = new ArrayList<>();
private int scoreboardUpdateTaskId = -1;
public EventScoreboardBuilder(Scorable scorable, int topPlaces, int aroundPlaces, int bottomPlaces) {
this(scorable, topPlaces, aroundPlaces, bottomPlaces, Comparator.comparingInt(EventScoreEntry::score).reversed());
}
public EventScoreboardBuilder(Scorable scorable, int topPlaces, int aroundPlaces, int bottomPlaces, Comparator<EventScoreEntry> scoreComparator) {
this.scorable = scorable;
this.topPlaces = topPlaces;
this.aroundPlaces = aroundPlaces;
this.bottomPlaces = bottomPlaces;
this.scoreComparator = scoreComparator;
}
public Scoreboard buildFor(Player p) {
List<EventScoreEntry> scoreList = new ArrayList<>(this.playerScores);
Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
Objective objective = scoreboard.registerNewObjective(
"event", "dummy",
Component.text(
"Scoreboard %s".formatted(this.scorable.getScoreboardName()),
NamedTextColor.GOLD, TextDecoration.BOLD
)
);
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
objective.numberFormat(NumberFormat.blank());
UUID uuid = p.getUniqueId();
scoreList.sort(this.scoreComparator);
int size = scoreList.size();
int playerIndex = IntStream.range(0, size)
.filter(i -> scoreList.get(i).playerUuid().equals(uuid))
.findFirst()
.orElse(0);
IntStream top = IntStream.range(0, Math.min(this.topPlaces, size));
int aroundStart = Math.max(0, playerIndex - this.aroundPlaces);
int aroundEnd = Math.min(size, playerIndex + this.aroundPlaces + 1);
IntStream around = IntStream.range(aroundStart, aroundEnd);
IntStream bottom = IntStream.range(Math.max(0, size - this.bottomPlaces), size);
int threshold = this.topPlaces + this.bottomPlaces + this.aroundPlaces * 2 + 1;
IntStream indices;
if (size <= threshold) {
indices = IntStream.range(0, size);
} else if (playerIndex <= this.topPlaces + this.aroundPlaces) {
indices = IntStream.concat(
IntStream.range(0, Math.min(size, playerIndex + this.aroundPlaces + 1)),
bottom
);
} else if (playerIndex >= size - this.bottomPlaces - this.aroundPlaces - 1) {
indices = IntStream.concat(
top,
IntStream.range(Math.max(0, playerIndex - this.aroundPlaces), size)
);
} else {
indices = IntStream.concat(IntStream.concat(top, around), bottom);
}
int[] display = indices.distinct().sorted().toArray();
List<String> lines = new ArrayList<>();
int prevIdx = -1;
int sepNo = 0;
for (int idx : display) {
if (prevIdx != -1 && idx > prevIdx + 1) {
lines.add(this.separatorLine(sepNo++));
}
EventScoreEntry entry = scoreList.get(idx);
if(!entry.playerUuid().equals(p.getUniqueId())) {
lines.add(this.formattedLine(idx, entry.name(), entry.score()));
} else {
lines.add(ChatColor.YELLOW + this.formattedLine(idx, entry.name(), entry.score()) + ChatColor.RESET);
}
prevIdx = idx;
}
int score = lines.size();
for (String line : lines) {
objective.getScore(line).setScore(score--);
}
return scoreboard;
}
private String separatorLine(int n) {
return ChatColor.GRAY + "..." + ChatColor.RESET + " ".repeat(n);
}
public void startAutomaticUpdates() {
this.scoreboardUpdateTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(
Main.instance(),
this::updateScoreboards,
Ticks.TICKS_PER_SECOND,
Ticks.TICKS_PER_SECOND
);
}
public void stopAutomaticUpdates() {
if(this.scoreboardUpdateTaskId != -1) Bukkit.getScheduler().cancelTask(this.scoreboardUpdateTaskId);
this.scoreboardUpdateTaskId = -1;
}
private void updateScore(Player p) {
EventScoreEntry previousEntry = this.playerScores.stream()
.filter(entry -> entry.playerUuid().equals(p.getUniqueId()))
.findFirst()
.orElse(null);
if(previousEntry == null) {
this.playerScores.add(new EventScoreEntry(p.getUniqueId(), p.getName(), this.scorable.getScore(p)));
return;
}
int currentScore = this.scorable.getScore(p);
if(previousEntry.score() < currentScore) {
this.playerScores.removeIf(entry -> entry.playerUuid().equals(p.getUniqueId()));
this.playerScores.add(new EventScoreEntry(p.getUniqueId(), p.getName(), currentScore));
}
}
public void updateScoreboards() {
Bukkit.getOnlinePlayers().forEach(player -> {
this.updateScore(player);
Scoreboard scoreboard = this.buildFor(player);
player.setScoreboard(scoreboard);
});
}
private String formattedLine(int place, String name, int score) {
name = this.trimName(name);
return "%s. %s : %s".formatted(place+1, name, score);
}
public List<String> getScores() {
List<EventScoreEntry> scoreList = new ArrayList<>(this.playerScores);
scoreList.sort(this.scoreComparator);
ArrayList<String> result = new ArrayList<>();
for(int i = 0; i < scoreList.size(); i++) {
result.add(this.formattedLine(i, scoreList.get(i).name(), scoreList.get(i).score()));
}
return result;
}
private String trimName(String name) {
if (name == null) return "Unknown";
if (name.length() > 12) {
return name.substring(0, 12);
}
return name;
}
}
+1
View File
@@ -4,4 +4,5 @@ include 'core'
include 'craftattack'
include 'common'
include 'varo'
include 'event'