Compare commits

..

37 Commits

Author SHA1 Message Date
fc6fd9ebb5 Merge branch 'master-big-events' 2025-12-23 22:34:51 +01:00
84edbcc0e4 Merge branch 'master-big-events' 2025-12-23 22:34:33 +01:00
9ec883d6ad made Scoreboard more beautiful 2025-12-23 22:33:32 +01:00
2b0c7c1a9e added countdown and sounds 2025-12-23 21:43:25 +01:00
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
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
8953a19400 added Bedrock player warning in joinEvent method to handle potential compatibility issues 2025-12-23 19:52:17 +01:00
85065bcc73 made SignEditListener formatting consistent and optimized ReportJoinListener to handle admin notifications asynchronously 2025-12-23 19:14:53 +01:00
2209b42766 fixed wrong reports info filtering 2025-12-23 19:11:59 +01:00
bf11bb0b70 fixed wrong reports info filtering 2025-12-23 19:05:53 +01:00
b98d33af40 added report reminder on admin join 2025-12-23 18:56:30 +01:00
0d6b21701f Merge remote-tracking branch 'origin/master' 2025-12-23 15:50:41 +01:00
b3240cdb22 fixed maximum of 8 settings per category 2025-12-23 15:50:23 +01:00
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
71f2da8e99 introduced AntiIllegalSignCharacters appliance to prevent usage of illegal characters in sign edits 2025-12-22 18:44:22 +01:00
a257b604ea updated InventoryTrackerListener to monitor InventoryOpenEvent with high priority and prevent processing of cancelled events 2025-12-21 09:32:40 +01:00
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
bd883a4fa1 added big events in craftattack plugin 2025-12-20 18:23:06 +01:00
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
5cda58408a Merge remote-tracking branch 'origin/master' into master-big-events 2025-12-20 17:45:26 +01:00
9767896cde updated OutlawedCommand and Outlawed to rename vogelfrei to PVP, adjusted related messages accordingly 2025-12-20 11:51:30 +01:00
f0e0cfbb85 fixed scoreboard order 2025-12-19 14:33:01 +01:00
de112f7e13 added timer 2025-12-19 14:04:36 +01:00
215259c6b9 added 'this' qualifier in Bloodmoon 2025-12-19 13:01:01 +01:00
36520a87f9 moved automatic scoreboard update start to EventController 2025-12-19 12:58:10 +01:00
914aaff10b added deathrun listener for border; fixed listener problem when disabling appliance 2025-12-13 16:02:23 +01:00
e015bbb356 added unsuspectedKick and crashKick features to Kick appliance; updated AcInform to include related commands 2025-12-13 15:00:48 +01:00
ec262710ec removed possibility for multiple events 2025-12-13 13:50:42 +01:00
1ac19014c1 introduced AntiInventoryMove appliance with related listeners and optimized admin notification logic in AcInform 2025-12-13 13:45:37 +01:00
dd1518fce4 split Event into Scorable and Event; removed AbstractEvent; added list, select and unselect commands 2025-12-12 19:30:21 +01:00
04cb233604 WIP: moved scores and score updates to EventScoreboardBuilder 2025-12-12 12:09:25 +01:00
2ff95f8450 changed scoreboard package name to lowercase 2025-12-02 12:52:18 +01:00
6ed48895ca made appliance ignore destruct when no listeners 2025-12-01 22:29:13 +01:00
bff8cf24cd added AbstractEvent and basic Scoreboard functionality 2025-12-01 22:28:15 +01:00
212f84b6de added start, stop and getScore methods to Event 2025-11-30 18:07:15 +01:00
f7430c8fc8 added possibility to disable appliances 2025-11-28 21:08:27 +01:00
c81a2d2161 started big events 2025-11-28 16:52:42 +01:00
42 changed files with 1390 additions and 84 deletions

View File

@@ -23,4 +23,8 @@ public class CraftAttackReportRepository extends ReportRepository {
ReportUrl.class ReportUrl.class
); );
} }
public ReqResp<AllReports> queryAllReports() {
return this.get("reports", AllReports.class);
}
} }

View File

@@ -21,6 +21,11 @@ public abstract class ReportRepository extends HttpRepository {
public record ReportUrl(@NotNull String url) { public record ReportUrl(@NotNull String url) {
} }
public enum Status {
open,
closed,
}
public record PlayerReports( public record PlayerReports(
List<Report> from_self, List<Report> from_self,
List<Report> to_self List<Report> to_self
@@ -31,11 +36,17 @@ public abstract class ReportRepository extends HttpRepository {
@Nullable Long created, @Nullable Long created,
@Nullable Status status, @Nullable Status status,
@NotNull String url @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
) { }
} }
} }

View File

@@ -1,6 +1,8 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report; package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.common.api.repositories.ReportRepository; import eu.mhsl.craftattack.spawn.common.api.repositories.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.Main;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.repositories.CraftAttackReportRepository; 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.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; 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() 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(); .toList();
List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed(); 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()); 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 @Override
@NotNull @NotNull
protected List<ApplianceCommand<?>> commands() { protected List<ApplianceCommand<?>> commands() {

View File

@@ -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));
}
}

View File

@@ -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())
);
}
}

View File

@@ -128,12 +128,12 @@ public class Settings extends Appliance {
if(categorizedSettings.isEmpty()) return; if(categorizedSettings.isEmpty()) return;
for(int i = 0; i < categorizedSettings.size(); i++) { for(int i = 0; i < categorizedSettings.size(); i++) {
int slot = row.get() * 9 + i % 9; if(i % 9 == 0 && i != 0) {
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
if(i % 9 == 8) {
row.incrementAndGet(); row.incrementAndGet();
} }
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
} }
row.incrementAndGet(); row.incrementAndGet();
}); });
@@ -143,12 +143,12 @@ public class Settings extends Appliance {
.toList(); .toList();
for(int i = 0; i < uncategorizedSettings.size(); i++) { for(int i = 0; i < uncategorizedSettings.size(); i++) {
int slot = row.get() * 9 + i % 9; if(i % 9 == 0 && i != 0) {
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
if(i % 9 == 8) {
row.incrementAndGet(); row.incrementAndGet();
} }
int slot = row.get() * 9 + i % 9;
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
} }
player.openInventory(inventory); player.openInventory(inventory);

View File

@@ -5,15 +5,10 @@ import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance; import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Boat; import org.bukkit.entity.Boat;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class AntiBoatFreecam extends Appliance { public class AntiBoatFreecam extends Appliance {
private static final float MAX_YAW_OFFSET = 106.0f; private static final float MAX_YAW_OFFSET = 106.0f;
private final Map<Player, Float> violatedPlayers = new HashMap<>();
public AntiBoatFreecam() { public AntiBoatFreecam() {
Bukkit.getScheduler().runTaskTimerAsynchronously( Bukkit.getScheduler().runTaskTimerAsynchronously(
@@ -27,14 +22,12 @@ public class AntiBoatFreecam extends Appliance {
float yawDelta = wrapDegrees(playerYaw - boatYaw); float yawDelta = wrapDegrees(playerYaw - boatYaw);
if(Math.abs(yawDelta) <= MAX_YAW_OFFSET) return; if(Math.abs(yawDelta) <= MAX_YAW_OFFSET) return;
this.violatedPlayers.merge(player, 1f, Float::sum); Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
float violationCount = this.violatedPlayers.get(player);
if(violationCount != 1 && violationCount % 100 != 0) return;
Main.instance().getAppliance(AcInform.class).notifyAdmins(
"internal", "internal",
player.getName(), player.getName(),
"illegalBoatLookYaw", "illegalBoatLookYaw",
violationCount yawDelta,
3000
); );
}), }),
1L, 1L,

View File

@@ -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()
);
}
}

View File

@@ -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 == '§';
}
}

View File

@@ -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()
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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);
}
}

View File

@@ -11,14 +11,20 @@ import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AcInform extends Appliance { public class AcInform extends Appliance {
private final Map<String, Map<String, Long>> violationSlowdowns = new ConcurrentHashMap<>();
public void processCommand(@NotNull String[] args) { public void processCommand(@NotNull String[] args) {
String anticheatName = null; String anticheatName = null;
String playerName = null; String playerName = null;
String checkName = null; String checkName = null;
Float violationCount = null; Float violationCount = null;
int notifyEvery = 0;
for(int i = 0; i < args.length; i++) { for(int i = 0; i < args.length; i++) {
if(!args[i].startsWith("--")) continue; if(!args[i].startsWith("--")) continue;
@@ -36,13 +42,32 @@ public class AcInform extends Appliance {
case "--playerName" -> playerName = value; case "--playerName" -> playerName = value;
case "--check" -> checkName = value; case "--check" -> checkName = value;
case "--violationCount" -> violationCount = value.isEmpty() ? null : Float.valueOf(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); this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
} }
public void notifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float 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(); ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text();
NamedTextColor textColor = NamedTextColor.GRAY; NamedTextColor textColor = NamedTextColor.GRAY;
@@ -85,28 +110,42 @@ public class AcInform extends Appliance {
Component.newline() Component.newline()
.append(Component.text("", NamedTextColor.GRAY)) .append(Component.text("", NamedTextColor.GRAY))
.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)) .append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/report %s anticheat %s flagged %s", playerName, anticheatName, checkName))) .clickEvent(ClickEvent.suggestCommand(String.format("/report %s anticheat %s flagged %s", playerName, anticheatName, checkName)))
); );
component.append( component.append(
Component.text(" [", NamedTextColor.GRAY) Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("Kick", NamedTextColor.GOLD)) .append(Component.text("\u23F1", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY)) .append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/kick %s", playerName))) .clickEvent(ClickEvent.suggestCommand(String.format("/kick %s", playerName)))
); );
component.append( component.append(
Component.text(" [", NamedTextColor.GRAY) 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)) .append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/panicban %s", playerName))) .clickEvent(ClickEvent.suggestCommand(String.format("/panicban %s", playerName)))
); );
component.append( component.append(
Component.text(" [", NamedTextColor.GRAY) Component.text(" [", NamedTextColor.GRAY)
.append(Component.text("Spectate/Teleport", NamedTextColor.GOLD)) .append(Component.text("\uD83D\uDC41", NamedTextColor.GOLD))
.append(Component.text("]", NamedTextColor.GRAY)) .append(Component.text("]", NamedTextColor.GRAY))
.clickEvent(ClickEvent.suggestCommand(String.format("/grim spectate %s", playerName))) .clickEvent(ClickEvent.suggestCommand(String.format("/grim spectate %s", playerName)))
); );

View File

@@ -1,9 +1,14 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.kick; 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.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; 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 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.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -25,9 +30,36 @@ public class Kick extends Appliance {
).applyKick(player); ).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 @Override
@NotNull @NotNull
protected List<ApplianceCommand<?>> commands() { protected List<ApplianceCommand<?>> commands() {
return List.of(new KickCommand()); return List.of(
new KickCommand(),
new KickUnsuspectedCommand(),
new KickCrashCommand()
);
} }
} }

View File

@@ -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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -10,6 +10,7 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.reflections.Reflections; import org.reflections.Reflections;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; 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(String.format("Loaded %d repositories!", this.repositoryLoader.getRepositories().size()));
Main.logger().info("Loading appliances..."); 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(applianceClass -> !disabledAppliances.contains(applianceClass.getSimpleName()))
.filter(appliance -> { .filter(appliance -> {
Appliance.Flags flags = appliance.getAnnotation(Appliance.Flags.class); 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); 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(String.format("Loaded %d appliances!", this.appliances.size()));
Main.logger().info("Initializing appliances..."); Main.logger().info("Initializing appliances...");
this.appliances.forEach(appliance -> { this.appliances.stream()
appliance.onEnable(); .filter(appliance -> {
appliance.initialize(this); 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())); Main.logger().info(String.format("Initialized %d appliances!", this.appliances.size()));
if(Configuration.pluginConfig.getBoolean("httpServerEnabled", true)) { if(Configuration.pluginConfig.getBoolean("httpServerEnabled", true)) {
@@ -85,17 +89,34 @@ public final class Main extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
Main.logger().info("Disabling appliances..."); Main.logger().info("Disabling appliances...");
this.appliances.forEach(appliance -> { this.appliances.forEach(appliance -> appliance.disableSequence(this));
Main.logger().info("Disabling " + appliance.getClass().getSimpleName());
appliance.onDisable();
appliance.destruct(this);
});
HandlerList.unregisterAll(this); HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTasks(this); Bukkit.getScheduler().cancelTasks(this);
Main.logger().info("Disabled " + this.appliances.size() + " appliances!"); 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) { public <T extends Appliance> T getAppliance(Class<T> clazz) {
return this.appliances.stream() return this.appliances.stream()
.filter(clazz::isInstance) .filter(clazz::isInstance)

View File

@@ -28,14 +28,16 @@ public class WebsiteHook extends HttpHook {
return HttpServer.nothing; 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 -> { 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))); SpawnEvent.call(new ReportCreatedEvent(new ReportCreatedEvent.CreatedReport(createdReport.reporter, createdReport.reported, createdReport.reason)));
return HttpServer.nothing; return HttpServer.nothing;
})); }));
record CreatedStrike(UUID uuid) {} record CreatedStrike(UUID uuid) {}
this.addAction("strike", new JsonAction<>(CreatedStrike.class, createdStrike -> { 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))); SpawnEvent.call(new StrikeCreatedEvent(new StrikeCreatedEvent.CreatedStrike(createdStrike.uuid)));
return HttpServer.nothing; return HttpServer.nothing;
})); }));

View File

@@ -28,6 +28,7 @@ public abstract class Appliance {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface Flags { public @interface Flags {
boolean enabled() default true; boolean enabled() default true;
boolean autoload() default true;
} }
private String localConfigPath; private String localConfigPath;
@@ -101,9 +102,20 @@ public abstract class Appliance {
} }
public void destruct(@NotNull JavaPlugin plugin) { public void destruct(@NotNull JavaPlugin plugin) {
if(this.listeners == null) return;
this.listeners.forEach(HandlerList::unregisterAll); 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) { protected <T extends Appliance> T queryAppliance(Class<T> clazz) {
return Main.instance().getAppliance(clazz); return Main.instance().getAppliance(clazz);
} }

View File

@@ -4,6 +4,8 @@ import org.bukkit.event.Event;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class ReportCreatedEvent extends Event { public class ReportCreatedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList(); private static final HandlerList HANDLERS = new HandlerList();
@Override @Override
@@ -15,7 +17,7 @@ public class ReportCreatedEvent extends Event {
return HANDLERS; return HANDLERS;
} }
public record CreatedReport(String reporter, String reported, String reason) {} public record CreatedReport(UUID reporter, UUID reported, String reason) {}
private final CreatedReport report; private final CreatedReport report;
public ReportCreatedEvent(CreatedReport report) { public ReportCreatedEvent(CreatedReport report) {

View File

@@ -1,10 +1,16 @@
package eu.mhsl.craftattack.spawn.core.util.entity; package eu.mhsl.craftattack.spawn.core.util.entity;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Statistic; import org.bukkit.Statistic;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class PlayerUtils { public class PlayerUtils {
public static void resetStatistics(Player player) { public static void resetStatistics(Player player) {
for(Statistic statistic : Statistic.values()) { 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);
}
} }

View File

@@ -92,10 +92,10 @@ public class Bloodmoon extends Appliance {
private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new WeakHashMap<>(); private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new WeakHashMap<>();
private long lastBloodmoonStartTick = 0; private long lastBloodmoonStartTick = 0;
public final int ticksPerDay = 24000; 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; public final int preStartMessageTicks = Ticks.TICKS_PER_SECOND * 50;
private final int bloodmoonFreeDaysAtStart = 3; 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 bloodmoonDayInterval = 30;
private final int minBonusDrops = 1; private final int minBonusDrops = 1;
private final int maxBonusDrops = 4; private final int maxBonusDrops = 4;

View File

@@ -19,7 +19,7 @@ public class BloodmoonSetting extends BoolSetting implements CategorizedSetting
@Override @Override
public SettingCategory category() { public SettingCategory category() {
return SettingCategory.Misc; // TODO: mehr als 8 bug fixen return SettingCategory.Gameplay;
} }
@Override @Override

View File

@@ -51,14 +51,15 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
void askForConfirmation(Player player) { void askForConfirmation(Player player) {
Component confirmationMessage = switch(this.getLawStatus(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 DISABLED -> Component.text("Wenn du den PVP-Modus aktivierst, darfst du von allen anderen PVP-Spielern grundlos angegriffen werden." +
case VOLUNTARILY -> Component.text("Wenn du Vogelfrei deaktivierst, darfst du nicht mehr grundlos von anderen Spielern angegriffen werden."); " Du kannst den PVP-Modus erst wieder nach " + this.timeoutInMs / 1000 / 60 / 60 + " Stunden deaktivieren!");
case FORCED -> Component.text("Du darfst zurzeit deinen Vogelfreistatus nicht ändern, da dieser als Strafe auferlegt wurde!"); 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); String command = String.format("/%s confirm", OutlawedCommand.commandName);
Component changeText = Component.text( Component changeText = Component.text(
String.format( 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 command
), ),
NamedTextColor.GOLD NamedTextColor.GOLD
@@ -74,11 +75,11 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
void switchLawStatus(Player player) throws OutlawChangeNotPermitted { void switchLawStatus(Player player) throws OutlawChangeNotPermitted {
if(this.getLawStatus(player).equals(Status.FORCED)) { 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)) { 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); 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) { public Component getStatusDescription(Status status) {
return switch(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)); .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( .append(Component.text(
"Du darfst von allen anderen vogelfreien Spielern angegriffen und getötet werden!" + "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!", "Wenn du getötet wirst, müssen andere Spieler deine Items *nicht* zurückerstatten!",
NamedTextColor.GOLD NamedTextColor.GOLD
)); ));
}; };
@@ -143,7 +144,7 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
if(this.isOutlawed(player)) { if(this.isOutlawed(player)) {
return Component.text("[☠]", NamedTextColor.RED) return Component.text("[☠]", NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text( .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!"
))); )));
} }

View File

@@ -8,7 +8,7 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
class OutlawedCommand extends ApplianceCommand.PlayerChecked<Outlawed> { class OutlawedCommand extends ApplianceCommand.PlayerChecked<Outlawed> {
public static final String commandName = "vogelfrei"; public static final String commandName = "pvp";
public OutlawedCommand() { public OutlawedCommand() {
super(commandName); super(commandName);

View File

@@ -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.Main;
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; 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.craftattack.api.repositories.EventRepository;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer; import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance; 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.event.Listener;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import org.geysermc.cumulus.form.SimpleForm;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
@@ -37,6 +39,11 @@ public class Event extends Appliance {
DONE DONE
} }
public enum EventType {
BIG,
SMALL
}
Countdown advertiseCountdown = new Countdown( Countdown advertiseCountdown = new Countdown(
120, 120,
announcementData -> Component.text() announcementData -> Component.text()
@@ -50,6 +57,7 @@ public class Event extends Appliance {
); );
public DisplayVillager.ConfigBound villager; public DisplayVillager.ConfigBound villager;
private boolean isOpen = false; private boolean isOpen = false;
private EventType eventType;
private AdvertisementStatus advertiseStatus = AdvertisementStatus.BEFORE; private AdvertisementStatus advertiseStatus = AdvertisementStatus.BEFORE;
private UUID roomId; private UUID roomId;
private final List<Reward> pendingRewards = new ArrayList<>(); 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", "")); 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!"); if(this.isOpen) throw new ApplianceCommand.Error("Es läuft derzeit bereits ein Event!");
this.eventType = type;
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { if(type.equals(EventType.SMALL)) {
ReqResp<EventRepository.CreatedRoom> sessionResponse = this.queryRepository(EventRepository.class).createSession(); Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.CreatedRoom> sessionResponse = this.queryRepository(EventRepository.class).createSession();
if(sessionResponse.status() != HttpStatus.OK) if(sessionResponse.status() != HttpStatus.OK)
throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status()); throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status());
this.isOpen = true; this.isOpen = true;
this.roomId = sessionResponse.data().uuid(); this.roomId = sessionResponse.data().uuid();
}); });
return;
}
this.isOpen = true;
} }
public void joinEvent(Player p) { public void joinEvent(Player p) {
this.joinEvent(p, false);
}
public void joinEvent(Player p, boolean ignoreBedrock) {
if(!this.isOpen) { if(!this.isOpen) {
p.sendMessage(Component.text("Zurzeit ist kein Event geöffnet.", NamedTextColor.RED)); p.sendMessage(Component.text("Zurzeit ist kein Event geöffnet.", NamedTextColor.RED));
return; return;
@@ -109,21 +127,42 @@ public class Event extends Appliance {
return; 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()); Main.instance().getLogger().info("Verbinde mit eventserver: " + p.getName());
p.sendMessage(Component.text("Authentifiziere...", NamedTextColor.GREEN)); p.sendMessage(Component.text("Authentifiziere...", NamedTextColor.GREEN));
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> { if(this.eventType.equals(EventType.SMALL)) {
ReqResp<EventRepository.QueueRoom.Response> queueResponse = this.queryRepository(EventRepository.class) Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
.queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId)); 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) { if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) {
p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED)); p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED));
return; return;
} }
p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN)); p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN));
PluginMessage.connect(p, this.localConfig().getString("connect-server-name")); PluginMessage.connect(p, this.localConfig().getString("connect-server-name"));
}); });
}
PluginMessage.connect(p, "grand-event");
} }
public void endEvent() { public void endEvent() {

View File

@@ -7,6 +7,10 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull; 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 class EventOpenSessionCommand extends ApplianceCommand<Event> {
public EventOpenSessionCommand() { public EventOpenSessionCommand() {
@@ -15,7 +19,17 @@ public class EventOpenSessionCommand extends ApplianceCommand<Event> {
@Override @Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().openEvent(); if(args.length == 1) {
this.getAppliance().openEvent(Event.EventType.SMALL);
} else {
this.getAppliance().openEvent(Event.EventType.valueOf(args[1]));
}
sender.sendMessage(Component.text("Event-Server gestartet!", NamedTextColor.GREEN)); 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;
}
} }

View File

@@ -22,6 +22,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@@ -29,14 +30,14 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import static java.util.Map.entry; 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 { public class ProjectStart extends Appliance {
private final int startMusicAt = 293; private final int startMusicAt = 130;
private final World startWorld = Bukkit.getWorld("world"); 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( 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( private final Countdown countdown = new Countdown(
@@ -47,7 +48,7 @@ public class ProjectStart extends Appliance {
); );
private final BlockCycle blockCycle = new BlockCycle( private final BlockCycle blockCycle = new BlockCycle(
this.glassLocation, this.glassLocation,
Material.RED_STAINED_GLASS, Material.WHITE_STAINED_GLASS,
List.of( List.of(
Material.RED_STAINED_GLASS, Material.RED_STAINED_GLASS,
Material.YELLOW_STAINED_GLASS, Material.YELLOW_STAINED_GLASS,
@@ -77,7 +78,7 @@ public class ProjectStart extends Appliance {
counter -> counter == this.startMusicAt, counter -> counter == this.startMusicAt,
counter -> this.glassLocation counter -> this.glassLocation
.getWorld() .getWorld()
.playSound(this.glassLocation, MUSIC_DISC_PRECIPICE, SoundCategory.RECORDS, 500f, 1f) .playSound(this.glassLocation, MUSIC_DISC_LAVA_CHICKEN, SoundCategory.RECORDS, 500f, 1f)
) )
); );
} }
@@ -128,7 +129,13 @@ public class ProjectStart extends Appliance {
Bukkit.getOnlinePlayers().forEach(player -> { Bukkit.getOnlinePlayers().forEach(player -> {
player.setFoodLevel(20); player.setFoodLevel(20);
player.setHealth(20); player.setHealth(20);
player.getInventory().clear(); if(player.getInventory().contains(Material.RECOVERY_COMPASS)) {
player.getInventory().clear();
player.give(new ItemStack(Material.RECOVERY_COMPASS));
} else {
player.getInventory().clear();
}
player.setGameMode(GameMode.SURVIVAL); player.setGameMode(GameMode.SURVIVAL);
player.setExp(0); player.setExp(0);
player.setLevel(0); player.setLevel(0);

9
event/build.gradle Normal file
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'
}

View File

@@ -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());
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()
);
}
}

View File

@@ -0,0 +1,31 @@
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;
import org.bukkit.event.entity.FoodLevelChangeEvent;
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);
}
@EventHandler
public void onHunger(FoodLevelChangeEvent event) {
if(!this.getAppliance().isBeforeStart()) event.setCancelled(true);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package eu.mhsl.craftattack.spawn.event.appliances.eventController;
public interface Event {
void start(long durationSeconds);
void stop();
long getDurationSeconds();
}

View File

@@ -0,0 +1,191 @@
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.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 void scheduleStart(long durationMinutes) {
if(this.selectedEvent == null) throw new ApplianceCommand.Error("There is no event selected!");
this.durationMinutes = durationMinutes;
this.countdown.start();
}
public void cancelStart() {
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")),
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<ApplianceCommand<?>> commands() {
return List.of(
new BigEventCommand(),
new JoinCraftattackCommand()
);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,177 @@
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.removeIf(e -> e.playerUuid().equals(uuid));
scoreList.add(new EventScoreEntry(uuid, p.getName(), this.scorable.getScore(p)));
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) {
this.playerScores.removeIf(entry -> entry.playerUuid().equals(p.getUniqueId()));
this.playerScores.add(new EventScoreEntry(p.getUniqueId(), p.getName(), this.scorable.getScore(p)));
}
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;
}
}

View File

@@ -4,4 +4,5 @@ include 'core'
include 'craftattack' include 'craftattack'
include 'common' include 'common'
include 'varo' include 'varo'
include 'event'