Compare commits

...

55 Commits

Author SHA1 Message Date
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
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
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
4d9548aafc resolved pr comments 2025-11-23 15:00:16 +01:00
4c63800189 Merge branch 'master' into develop-bloodmoon 2025-11-23 12:58:45 +00:00
b8725bc0f2 made bossbar shrink until end of bloodmoon, fixed horde spawning 2025-11-23 13:58:02 +01:00
c90d767f0f added todos 2025-11-21 09:12:17 +01:00
143f4ee8eb adjusted bloodmoon settings 2025-11-21 09:03:43 +01:00
ba2befb467 refactored event hierarchy; replaced SpawnEvent with direct Event implementation, added StrikeUpdateListener and refactored whitelist profile update logic 2025-11-16 12:02:49 +01:00
7a2b9b9763 updated Paper API dependency to 1.21.10-R0.1-SNAPSHOT across all modules 2025-11-16 12:00:58 +01:00
0b9dc5358d added HTTP hooks framework with actions for signup, report, and strike events; introduced SpawnEvent support for event broadcasting 2025-11-15 12:50:01 +01:00
448e9472db updated outlawed info messages 2025-11-09 20:36:21 +01:00
933c4c0db0 refactored InfoBars handling logic; replaced persistent data with direct visibility control 2025-11-09 19:32:44 +01:00
f27474016a added @Flags annotation to Appliance, disabled DeviceFingerprinting appliance by default 2025-11-09 19:31:25 +01:00
17e5b2e049 Merge branch 'develop-backendUpdate' 2025-11-09 19:21:39 +01:00
d3512cb2eb Merge remote-tracking branch 'origin/master' 2025-11-09 19:13:34 +01:00
7b19bfd39e Merge branch 'develop-deviceFingerprinting' 2025-11-09 19:13:18 +01:00
0ab67bb426 refactored strike handling logic; added ban determination and updated whitelist integration 2025-11-09 19:13:08 +01:00
29a362b580 updated default shortcut setting value 2025-11-09 16:08:28 +01:00
239094971c Revert "simplified event message handling logic in ChatMessagesListener"
This reverts commit db13a9f0a2.
2025-11-02 14:18:43 +01:00
53dbeff829 merged automatic start with hordes per player 2025-11-01 17:43:35 +01:00
b5da06fd49 Merge remote-tracking branch 'origin/develop-bloodmoon' into develop-bloodmoon
# Conflicts:
#	craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/bloodmoon/Bloodmoon.java
2025-11-01 17:32:36 +01:00
b55035f1f0 added timings and announcements for automatic bloodmoon start 2025-11-01 17:29:39 +01:00
91a28b4500 Merge branch 'master' into develop-backendUpdate 2025-10-27 17:11:01 +01:00
23af3ff784 Merge branch 'master' into develop-backendUpdate 2025-10-27 16:14:24 +01:00
c220479052 WIP: refactored report and feedback systems; updated repository models, APIs, and component utilities 2025-10-27 14:25:44 +01:00
0565f5de9e changed bloodmoon setting description 2025-10-20 16:28:00 +02:00
ff81c91661 changed horde spawning to delay for each player 2025-10-20 16:27:10 +02:00
1182cb2ed6 added todos 2025-10-19 21:35:03 +02:00
045f31d4ca removed horde announcement message and unnecessary copy of player location, added setting feedback when bloodmoon active 2025-10-19 21:26:24 +02:00
263fd85df7 fixed multiple horde tasks being started 2025-10-19 18:56:12 +02:00
d70b025502 added hordes, disabled setting when bloodmoon is active 2025-10-19 18:45:55 +02:00
e14c87c2fb added mob effects and Setting 2025-10-19 01:18:27 +02:00
9433495a52 added random drops and boss bar for Bloodmoon 2025-10-18 19:01:09 +02:00
4955e94306 started bloodmoon appliance 2025-10-17 19:38:10 +02:00
aad1fcafa6 added FingerprintData class and improved device fingerprinting logic 2025-10-05 15:46:45 +02:00
9fca7430a8 implemented working fingerprinting prototype 2025-10-05 13:24:47 +02:00
98 changed files with 2482 additions and 227 deletions

View File

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

View File

@@ -11,15 +11,14 @@ public class CraftAttackReportRepository extends ReportRepository {
public ReqResp<PlayerReports> queryReports(UUID player) { public ReqResp<PlayerReports> queryReports(UUID player) {
return this.get( return this.get(
"report", "users/%s/reports".formatted(player.toString()),
(parameters) -> parameters.addParameter("uuid", player.toString()),
PlayerReports.class PlayerReports.class
); );
} }
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) { public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post( return this.post(
"report", "reports",
data, data,
ReportUrl.class ReportUrl.class
); );

View File

@@ -23,19 +23,18 @@ public abstract class ReportRepository extends HttpRepository {
public record PlayerReports( public record PlayerReports(
List<Report> from_self, List<Report> from_self,
Object to_self List<Report> to_self
) { ) {
public record Report( public record Report(
@Nullable Reporter reported, @Nullable UUID reported,
@NotNull String subject, @NotNull String reason,
boolean draft, @Nullable Long created,
@NotNull String status, @Nullable Status status,
@NotNull String url @NotNull String url
) { ) {
public record Reporter( public enum Status {
@NotNull String username, open,
@NotNull String uuid closed,
) {
} }
} }
} }

View File

@@ -21,17 +21,18 @@ class ChatMessagesListener extends ApplianceListener<ChatMessages> {
public void onPlayerChatEvent(AsyncChatEvent event) { public void onPlayerChatEvent(AsyncChatEvent event) {
event.renderer( event.renderer(
(source, sourceDisplayName, message, viewer) -> (source, sourceDisplayName, message, viewer) ->
Component.text() Component.text("")
.append(this.getAppliance().getReportablePlayerName(source)) .append(this.getAppliance().getReportablePlayerName(source))
.append(Component.text(" > ").color(TextColor.color(Color.GRAY.asRGB()))) .append(Component.text(" > ").color(TextColor.color(Color.GRAY.asRGB())))
.append(message).color(TextColor.color(Color.SILVER.asRGB())) .append(message).color(TextColor.color(Color.SILVER.asRGB()))
.build()
); );
} }
@EventHandler(priority = EventPriority.HIGH) @EventHandler(priority = EventPriority.HIGH)
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
if(event.joinMessage() == null) return; boolean wasHidden = event.joinMessage() == null;
event.joinMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> { IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return; if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage( player.sendMessage(
@@ -44,7 +45,9 @@ class ChatMessagesListener extends ApplianceListener<ChatMessages> {
@EventHandler @EventHandler
public void onPlayerLeave(PlayerQuitEvent event) { public void onPlayerLeave(PlayerQuitEvent event) {
if(event.quitMessage() == null) return; boolean wasHidden = event.quitMessage() == null;
event.quitMessage(null);
if(wasHidden) return;
IteratorUtil.onlinePlayers(player -> { IteratorUtil.onlinePlayers(player -> {
if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return; if(!Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class)) return;
player.sendMessage( player.sendMessage(

View File

@@ -46,6 +46,4 @@ public class InfoBarSetting extends MultiBoolSetting<InfoBarSetting.InfoBarConfi
public Class<?> dataType() { public Class<?> dataType() {
return InfoBarConfiguration.class; return InfoBarConfiguration.class;
} }
} }

View File

@@ -1,25 +1,18 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars; package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
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.common.appliances.metaGameplay.infoBars.bars.MsptBar; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.MsptBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.PlayerCounterBar; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.PlayerCounterBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsBar; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsBar;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class InfoBars extends Appliance { public class InfoBars extends Appliance {
private final NamespacedKey infoBarKey = new NamespacedKey(Main.instance(), "infobars");
private final List<Bar> infoBars = List.of( private final List<Bar> infoBars = List.of(
new TpsBar(), new TpsBar(),
new MsptBar(), new MsptBar(),
@@ -27,41 +20,19 @@ public class InfoBars extends Appliance {
); );
public void showAllEnabled(Player player) { public void showAllEnabled(Player player) {
this.getEnabledBars(player).forEach(bar -> this.show(player, bar)); InfoBarSetting.InfoBarConfiguration config = Settings.instance().getSetting(player, Settings.Key.InfoBars, InfoBarSetting.InfoBarConfiguration.class);
this.setVisible(player, MsptBar.name, config.mspt());
this.setVisible(player, PlayerCounterBar.name, config.playerCounter());
this.setVisible(player, TpsBar.name, config.tps());
} }
public void hideAllEnabled(Player player) { public void setVisible(Player player, String bar, boolean visible) {
this.getEnabledBars(player).forEach(bar -> this.hide(player, bar));
this.setEnabledBars(player, List.of());
}
public void show(Player player, String bar) {
this.validateBarName(bar); this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getEnabledBars(player)); if(visible) {
existingBars.add(bar); player.showBossBar(this.getBarByName(bar).getBossBar());
player.showBossBar(this.getBarByName(bar).getBossBar()); } else {
this.setEnabledBars(player, existingBars); player.hideBossBar(this.getBarByName(bar).getBossBar());
} }
public void hide(Player player, String bar) {
this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getEnabledBars(player));
existingBars.remove(bar);
player.hideBossBar(this.getBarByName(bar).getBossBar());
this.setEnabledBars(player, existingBars);
}
private List<String> getEnabledBars(Player player) {
PersistentDataContainer container = player.getPersistentDataContainer();
if(!container.has(this.infoBarKey)) return List.of();
return container.get(this.infoBarKey, PersistentDataType.LIST.strings());
}
private void setEnabledBars(Player player, List<String> bars) {
Bukkit.getScheduler().runTask(
Main.instance(),
() -> player.getPersistentDataContainer().set(this.infoBarKey, PersistentDataType.LIST.strings(), bars)
);
} }
private Bar getBarByName(String name) { private Bar getBarByName(String name) {
@@ -79,13 +50,7 @@ public class InfoBars extends Appliance {
@Override @Override
public void onEnable() { public void onEnable() {
Settings.instance().declareSetting(InfoBarSetting.class); Settings.instance().declareSetting(InfoBarSetting.class);
Settings.instance().addChangeListener(InfoBarSetting.class, player -> { Settings.instance().addChangeListener(InfoBarSetting.class, this::showAllEnabled);
this.hideAllEnabled(player);
InfoBarSetting.InfoBarConfiguration config = Settings.instance().getSetting(player, Settings.Key.InfoBars, InfoBarSetting.InfoBarConfiguration.class);
if(config.mspt()) this.show(player, MsptBar.name);
if(config.playerCounter()) this.show(player, PlayerCounterBar.name);
if(config.tps()) this.show(player, TpsBar.name);
});
} }
@Override @Override

View File

@@ -6,6 +6,7 @@ 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;
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.text.ComponentUtil;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
@@ -19,7 +20,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
public class Report extends Appliance { public class Report extends Appliance {
public static Component helpText() { public static Component helpText() {
@@ -78,7 +81,7 @@ public class Report extends Appliance {
.appendNewline() .appendNewline()
.append( .append(
Component Component
.text(createdReport.data().url(), NamedTextColor.GRAY) // URL mit Weltkugel-Emoji .text(createdReport.data().url(), NamedTextColor.GRAY)
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url())) .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url()))
) )
.appendNewline() .appendNewline()
@@ -128,43 +131,50 @@ public class Report extends Appliance {
return; return;
} }
List<ReportRepository.PlayerReports.Report> reports = userReports Function<List<ReportRepository.PlayerReports.Report>, List<ReportRepository.PlayerReports.Report>> filterClosed = reports -> reports.stream()
.data() .filter(report -> Objects.equals(report.status(), ReportRepository.PlayerReports.Report.Status.closed))
.from_self() .toList();
.stream()
.filter(report -> !report.draft())
.toList()
.reversed();
if(reports.isEmpty()) { List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed();
issuer.sendMessage( List<ReportRepository.PlayerReports.Report> reportsToSelf = filterClosed.apply(userReports.data().to_self()).reversed();
Component.text()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY))
);
return;
}
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text() ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.newline()) .append(Component.text(
.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD)) !reportsToSelf.isEmpty()
.appendNewline(); ? "Du wurdest insgesamt %d mal von anderen Spielern gemeldet.".formatted(reportsToSelf.size())
: "Du wurdest von keinem anderen Spieler gemeldet.",
NamedTextColor.GOLD)
);
reports.forEach(report -> { component.appendNewline();
component
.append(Component.text(" - ", NamedTextColor.WHITE)) component.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD));
.append( reportsToOthers.forEach(report -> {
report.reported() != null Component button = Component.text("[\uD83D\uDC41/\uD83D\uDD8A]")
? Component.text(report.reported().username(), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW)
)
.append(Component.text(String.format(": %s", report.subject()), NamedTextColor.GRAY))
.clickEvent(ClickEvent.openUrl(report.url())) .clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um den Report einzusehen.", NamedTextColor.GOLD))); .hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(report.url())));
component.appendNewline();
Component reportedDisplayName = report.reported() != null
? Component.text(Optional.ofNullable(Bukkit.getOfflinePlayer(report.reported()).getName()).orElse(report.reported().toString()), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW);
component
.appendNewline()
.append(Component.text(" \u27A1 ", NamedTextColor.GRAY))
.append(button)
.append(Component.text(" du gegen ", NamedTextColor.GRAY))
.append(reportedDisplayName)
.append(Component.text(String.format(": %s", report.reason()), NamedTextColor.GRAY));
}); });
if(reportsToOthers.isEmpty()) {
component
.appendNewline()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY));
}
issuer.sendMessage(component.build()); issuer.sendMessage(component.build());
} }

View File

@@ -38,7 +38,8 @@ public class Settings extends Appliance {
BorderWarning, BorderWarning,
LocatorBar, LocatorBar,
InfoBars, InfoBars,
CoordinateDisplay CoordinateDisplay,
Bloodmoon
} }
public static Settings instance() { public static Settings instance() {

View File

@@ -25,7 +25,7 @@ public class SettingsShortcutSetting extends BoolSetting implements CategorizedS
@Override @Override
protected Boolean defaultValue() { protected Boolean defaultValue() {
return false; return true;
} }
@Override @Override

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,38 @@
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;
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,21 @@
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.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
class InventoryTrackerListener extends ApplianceListener<AntiInventoryMove> {
@EventHandler
public void onOpen(InventoryOpenEvent event) {
if(!(event.getPlayer() instanceof Player player)) 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

@@ -0,0 +1,177 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import net.kyori.adventure.resource.ResourcePackInfo;
import net.kyori.adventure.resource.ResourcePackRequest;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import org.jetbrains.annotations.NotNull;
import spark.Response;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.*;
@Appliance.Flags(enabled = false)
public class DeviceFingerprinting extends Appliance {
public record PackInfo(@NotNull String url, @NotNull UUID uuid, @NotNull String hash) {
private static final String failingUrl = "http://127.0.0.1:0";
public PackInfo asFailing() {
return new PackInfo(failingUrl, this.uuid, this.hash);
}
}
public enum PackStatus {
UNCACHED,
CACHED,
INVALID;
public static PackStatus fromBukkitStatus(PlayerResourcePackStatusEvent.Status status) {
return switch(status) {
case DISCARDED -> CACHED;
case FAILED_DOWNLOAD -> UNCACHED;
default -> INVALID;
};
}
}
public enum PlayerStatus {
PREPARATION,
TESTING,
FINISHED,
NEW
}
private List<PackInfo> packs;
private final Map<Player, FingerprintData> fingerprints = new WeakHashMap<>();
private final UUID basePackId = UUID.randomUUID();
@Override
public void onEnable() {
this.packs = this.readPacksFromConfig();
}
public void startFingerprinting(Player player) {
this.fingerprints.put(player, FingerprintData.create(player));
Main.logger().info(String.format("Sending base ressource-pack with id '%s' to '%s'%n", this.basePackId, player.getName()));
this.sendPack(player, new PackInfo("http://localhost:8080/api/devicefingerprinting/base.zip", this.basePackId, "3296e8bdd30b4f7cffd11c780a1dc70da2948e71"));
}
public void onPackUpdate(Player player, UUID packId, PlayerResourcePackStatusEvent.Status status) {
if(!this.fingerprints.containsKey(player)) return;
FingerprintData playerFingerprint = this.fingerprints.get(player);
if(!playerFingerprint.isInTestingOrPreparation()) return;
if(packId.equals(this.basePackId)) {
Main.logger().info(String.format("Base pack for '%s' updated: '%s'", player.getName(), status));
if(status != PlayerResourcePackStatusEvent.Status.ACCEPTED) return;
Main.logger().info(String.format("Base pack loaded successfully, sending now all packs to '%s'...", player.getName()));
playerFingerprint.setTesting();
this.packs.forEach(pack -> this.sendPack(player, pack.asFailing()));
return;
}
PackInfo pack = this.packs.stream()
.filter(packInfo -> Objects.equals(packInfo.uuid, packId))
.findAny()
.orElse(null);
if(pack == null) return;
int packIndex = this.packs.indexOf(pack);
List<PackStatus> pendingPacks = playerFingerprint.getPendingPacks();
PackStatus newPackStatus = PackStatus.fromBukkitStatus(status);
if(newPackStatus == PackStatus.INVALID) return;
pendingPacks.set(packIndex, newPackStatus);
playerFingerprint.updateFingerprint();
if(Objects.requireNonNull(playerFingerprint.getStatus()) == PlayerStatus.NEW) {
Main.logger().info(String.format("Sending fingerprint packs to Player '%s', as it is a unseen Player!", player.getName()));
this.sendNewFingerprint(player, Objects.requireNonNull(playerFingerprint.getFingerPrint()));
}
}
private void sendNewFingerprint(Player player, long fingerprintId) {
for (int i = 0; i < this.packs.size(); i++) {
if ((fingerprintId & (1L << i)) != 0) {
PackInfo pack = this.packs.get(i);
this.sendPack(player, pack);
}
}
}
public void sendPack(Player player, PackInfo pack) {
player.sendResourcePacks(
ResourcePackRequest.resourcePackRequest()
.required(true)
.packs(ResourcePackInfo.resourcePackInfo(pack.uuid, URI.create(pack.url), pack.hash))
);
}
private List<DeviceFingerprinting.PackInfo> readPacksFromConfig() {
try (InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(Main.class.getResourceAsStream("/deviceFingerprinting/packs.json")))) {
Type packListType = new TypeToken<List<DeviceFingerprinting.PackInfo>>(){}.getType();
List<DeviceFingerprinting.PackInfo> packs = new Gson().fromJson(reader, packListType);
if (packs.isEmpty()) throw new IllegalStateException("No resource packs found in packs.json.");
return packs;
} catch (IOException e) {
throw new IllegalStateException("Failed to parse packs.json.", e);
}
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
apiBuilder.rawGet(
"base.zip",
(request, response) -> this.servePack("base.zip", response)
);
for(int i = 0; i < this.packs.size(); i++) {
int packIndex = i;
apiBuilder.rawGet(
String.format("packs/%d", i),
(request, response) -> this.servePack(String.valueOf(packIndex), response)
);
}
}
private Object servePack(String name, Response response) {
try {
String resourcePath = String.format("/deviceFingerprinting/packs/%s", name);
var inputStream = Main.class.getResourceAsStream(resourcePath);
if (inputStream == null) {
throw new IllegalStateException("Pack file not found: " + resourcePath);
}
response.header("Content-Type", "application/zip");
response.header("Content-Disposition", String.format("attachment; filename=\"pack-%s.zip\"", name));
var outputStream = response.raw().getOutputStream();
inputStream.transferTo(outputStream);
outputStream.close();
return HttpServer.nothing;
} catch (IOException e) {
throw new RuntimeException(String.format("Failed to serve pack '%s'", name), e);
}
}
public List<PackInfo> getPacks() {
return this.packs;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new PlayerJoinListener()
);
}
}

View File

@@ -0,0 +1,91 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import eu.mhsl.craftattack.spawn.core.Main;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class FingerprintData {
public final Player player;
private DeviceFingerprinting.PlayerStatus status;
private @Nullable Long fingerPrint;
private final List<DeviceFingerprinting.PackStatus> pendingPacks;
int packCount = Main.instance().getAppliance(DeviceFingerprinting.class).getPacks().size();
private FingerprintData(Player player) {
this.player = player;
this.status = DeviceFingerprinting.PlayerStatus.PREPARATION;
this.fingerPrint = null;
this.pendingPacks = Arrays.asList(new DeviceFingerprinting.PackStatus[this.packCount]);
}
public static FingerprintData create(Player player) {
return new FingerprintData(player);
}
public void setTesting() {
this.status = DeviceFingerprinting.PlayerStatus.TESTING;
}
public void updateFingerprint() {
long fingerPrint = 0;
for (int i = 0; i < this.pendingPacks.size(); i++) {
var status = this.pendingPacks.get(i);
if(status == null) return;
switch (status) {
case CACHED:
fingerPrint |= 1L << i;
break;
case UNCACHED:
break;
default:
return;
}
}
if(fingerPrint == 0) {
this.status = DeviceFingerprinting.PlayerStatus.NEW;
this.fingerPrint = this.createNewFingerprint();
Main.logger().info(String.format("Player %s's was marked as a new Player!", this.player.getName()));
} else {
this.status = DeviceFingerprinting.PlayerStatus.FINISHED;
this.fingerPrint = fingerPrint;
}
Main.logger().info(String.format("Player %s's fingerprint is '%s'", this.player.getName(), fingerPrint));
}
private long createNewFingerprint() {
long id = 0;
Random random = new Random();
for (int i = 0; i < this.packCount / 2; i++) {
while (true) {
int bitIndex = random.nextInt(this.packCount);
if ((id & (1L << bitIndex)) == 0) {
id |= 1L << bitIndex;
break;
}
}
}
return id;
}
public List<DeviceFingerprinting.PackStatus> getPendingPacks() {
return this.pendingPacks;
}
public DeviceFingerprinting.PlayerStatus getStatus() {
return this.status;
}
public @Nullable Long getFingerPrint() {
return this.fingerPrint;
}
public boolean isInTestingOrPreparation() {
return this.status == DeviceFingerprinting.PlayerStatus.TESTING || this.status == DeviceFingerprinting.PlayerStatus.PREPARATION;
}
}

View File

@@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
class PlayerJoinListener extends ApplianceListener<DeviceFingerprinting> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().startFingerprinting(event.getPlayer());
}
@EventHandler
public void onResourcePackEvent(PlayerResourcePackStatusEvent event) {
this.getAppliance().onPackUpdate(
event.getPlayer(),
event.getID(),
event.getStatus()
);
}
}

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

@@ -0,0 +1,5 @@
## Files originally from "TrackPack"
https://github.com/ALaggyDev/TrackPack/blob/main/README.md
Discovered by: [Laggy](https://github.com/ALaggyDev/) and [NikOverflow](https://github.com/NikOverflow)

View File

@@ -0,0 +1,33 @@
import zipfile
import hashlib
import uuid
import json
SERVER_URL = "http://localhost:8080/api/devicefingerprinting"
packs = []
def file_sha1(path):
h = hashlib.sha1()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
for i in range(0, 24):
path = f"packs/{i}"
with zipfile.ZipFile(path, mode="w") as zf:
zf.writestr(
"pack.mcmeta",
'{"pack":{"pack_format":22,"supported_formats":[22,1000],"description":"pack ' + str(i) + '"}}',
)
hash = file_sha1(path)
packs.append({
"url": f"{SERVER_URL}/packs/{i}",
"uuid": str(uuid.uuid4()),
"hash": hash
})
with open("packs.json", "w") as f:
json.dump(packs, f, indent=4)

View File

@@ -0,0 +1,122 @@
[
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/0",
"uuid": "b35f6e2f-1b50-4493-85be-fb18bd90f9bb",
"hash": "7a39af839ea6484431f7b707759546bea991d435"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/1",
"uuid": "71095b62-d5ef-4ab2-ba3b-3c1b403f5e34",
"hash": "a9192ee73df1c5cff2c188fac6e9e638a1e7b6ce"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/2",
"uuid": "a4dba0a2-f8f2-4a81-bbb2-a9a818820330",
"hash": "6b85b0eb54865dae70bbda89746d83717dc2a214"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/3",
"uuid": "79fa2dc4-8c84-45fc-a09f-d89906f0d900",
"hash": "c7abf7a316f7e8c98985e8317a8b649e824e9f79"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/4",
"uuid": "15702c9b-a22b-426d-b48a-3d65b0368e9a",
"hash": "10cd0e2c46f192deb87ac75c149827d44a713017"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/5",
"uuid": "3d702d41-8e2f-4920-8dd0-1fd2146da9fb",
"hash": "8ad517d259e800b88a38ff00ee6721d5656822f2"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/6",
"uuid": "c20a2e47-ef43-49da-a80d-adf238df3695",
"hash": "798677405a4fd678892e1cf55585c8c91f82e1e2"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/7",
"uuid": "7ce51b81-1263-4919-9f4e-bb749ffe6e2e",
"hash": "af473b8eb7572f35d307bede5f2e20f263c0d804"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/8",
"uuid": "0c70d586-fe48-4ffc-86b0-6b9ec3bfe045",
"hash": "2fb698ff88f2436637641f3b2e6792201feb5144"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/9",
"uuid": "c7af75a8-0b72-495d-a0ff-c1c40e229c13",
"hash": "cf660460798eecf451d639873cc1fedc4661db1b"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/10",
"uuid": "248dbce6-4b2a-44b5-b038-8d718b0ced99",
"hash": "a8ebe708d0f3747c76e4e5e68db5dcb561922706"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/11",
"uuid": "10979174-cb02-40eb-a754-275551ad608d",
"hash": "54961b48db1582a1a0981c8cc9be5ae0f3122cf3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/12",
"uuid": "a361cfa7-674c-4493-a4cf-4baff851f276",
"hash": "013719dc8da79c96b45a1c5319c20bffe1a56cc9"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/13",
"uuid": "24b39bdb-ada9-40ec-9e3a-132c74b81dc6",
"hash": "206898c6b6600d2648b2d79c61fc6255b19587d9"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/14",
"uuid": "158fc5b4-be2c-4f7a-98cb-af5993adcc90",
"hash": "061b266a7c526fb3a3152a4ea70ca5592e0b503c"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/15",
"uuid": "4f9097a7-be02-48ad-919c-f292307f8490",
"hash": "45a667a0fe06246defabca14ef1271fb6db5a1ac"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/16",
"uuid": "3ce31e60-7e8a-4fb1-8c6d-da9065bea798",
"hash": "75bb12e46203d49e89aa9a826d267552372758bc"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/17",
"uuid": "cd978e5c-3de0-4ada-8ec5-3a88a305eec6",
"hash": "5b20261f7be03e83e9c52307f1408b0c5e58358c"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/18",
"uuid": "75001e58-3999-4779-a1d1-43ab161770ce",
"hash": "544420cffb6c17113c06fb49eeba892c208719d3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/19",
"uuid": "6a7005a9-c2ca-476d-9a12-07d120ee121a",
"hash": "fcc066a4d3193b60b102e3d906ad8dc0b0fcf65b"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/20",
"uuid": "521c0d84-d82e-49ef-b096-d9b90f15aa19",
"hash": "4545835983ec7f07d02675a69181a80dc396f038"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/21",
"uuid": "c1b590c5-43fc-41e3-83c0-47f35b14f845",
"hash": "8d4c670eaefc0482734e839b72758226dde13bc3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/22",
"uuid": "43958a18-c087-4f2b-a6ea-066231606eb1",
"hash": "004282602f7bdbb7cd7724f23aae23876f224092"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/23",
"uuid": "4b91ac81-9de4-4c2b-a876-47e621496d10",
"hash": "dae68eae109e08ea4c4c943905502eb331939f64"
}
]

View File

@@ -0,0 +1 @@
!base.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
dependencies { dependencies {
compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4' implementation 'com.sparkjava:spark-core:2.9.4'

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,8 +50,13 @@ 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 -> {
Appliance.Flags flags = appliance.getAnnotation(Appliance.Flags.class);
if(flags == null) return true;
return flags.enabled();
})
.map(applianceClass -> { .map(applianceClass -> {
try { try {
return (Appliance) applianceClass.getDeclaredConstructor().newInstance(); return (Appliance) applianceClass.getDeclaredConstructor().newInstance();
@@ -58,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)) {
@@ -80,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

@@ -2,11 +2,13 @@ package eu.mhsl.craftattack.spawn.core.api.server;
import com.google.gson.Gson; import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.hooks.HttpHook;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance; import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import spark.Request; import spark.Request;
import spark.Spark; import spark.Spark;
import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -21,6 +23,11 @@ public class HttpServer {
Spark.get("/ping", (request, response) -> System.currentTimeMillis()); Spark.get("/ping", (request, response) -> System.currentTimeMillis());
Spark.post("/hook/:hookId", (request, response) -> {
HttpHook.Hook hook = HttpHook.Hook.valueOf(request.params(":hookId").toUpperCase());
return hook.getHook().runAction(request.headers(hook.getHeaderAction()), request, response);
});
Main.instance().getAppliances().forEach(appliance -> appliance.httpApi(new ApiBuilder(appliance))); Main.instance().getAppliances().forEach(appliance -> appliance.httpApi(new ApiBuilder(appliance)));
} }
@@ -43,12 +50,16 @@ public class HttpServer {
this.applianceName = appliance.getClass().getSimpleName().toLowerCase(); this.applianceName = appliance.getClass().getSimpleName().toLowerCase();
} }
public void rawGet(String path, BiFunction<Request, spark.Response, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req, resp)));
}
public void get(String path, Function<Request, Object> onCall) { public void get(String path, Function<Request, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req))); Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
} }
public void rawPost(String path, Function<Request, Object> onCall) { public void rawPost(String path, BiFunction<Request, spark.Response, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req))); Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req, resp)));
} }
public <TRequest> void post(String path, Class<TRequest> clazz, RequestProvider<TRequest, Request, Object> onCall) { public <TRequest> void post(String path, Class<TRequest> clazz, RequestProvider<TRequest, Request, Object> onCall) {
@@ -59,7 +70,7 @@ public class HttpServer {
}); });
} }
public String buildRoute(String path) { private String buildRoute(String path) {
return String.format("/api/%s/%s", this.applianceName, path); return String.format("/api/%s/%s", this.applianceName, path);
} }

View File

@@ -0,0 +1,55 @@
package eu.mhsl.craftattack.spawn.core.api.server.hooks;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.api.server.hooks.impl.WebsiteHook;
import spark.Request;
import spark.Response;
import java.util.HashMap;
import java.util.Map;
public abstract class HttpHook {
public enum Hook {
WEBSITE("x-webhook-action", new WebsiteHook());
private final String headerAction;
private final HttpHook hook;
Hook(String headerAction, HttpHook handler) {
this.headerAction = headerAction;
this.hook = handler;
this.hook.registerHooks();
}
public HttpHook getHook() {
return this.hook;
}
public String getHeaderAction() {
return this.headerAction;
}
}
public abstract static class Action {
public abstract Object run(Request request, Response response);
}
private final Map<String, Action> actions = new HashMap<>();
protected HttpHook() {
}
protected abstract void registerHooks();
protected void addAction(String name, Action action) {
this.actions.put(name, action);
}
public Object runAction(String action, Request request, Response response) {
if(!this.actions.containsKey(action)) {
Main.logger().warning(String.format("Webhook-Action '%s' not registered, skipping!", action));
return HttpServer.nothing;
}
return this.actions.get(action).run(request, response);
}
}

View File

@@ -0,0 +1,26 @@
package eu.mhsl.craftattack.spawn.core.api.server.hooks;
import com.google.gson.Gson;
import spark.Request;
import spark.Response;
import java.util.function.Function;
public class JsonAction<TRequest, TResponse> extends HttpHook.Action {
private final Function<TRequest, TResponse> handler;
private final Class<TRequest> requestClass;
private final Gson gson = new Gson();
public JsonAction(Class<TRequest> requestClass, Function<TRequest, TResponse> handler) {
this.requestClass = requestClass;
this.handler = handler;
}
@Override
public Object run(Request request, Response response) {
TRequest req = this.gson.fromJson(request.body(), this.requestClass);
response.type("application/json");
return this.gson.toJson(this.handler.apply(req));
}
}

View File

@@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.core.api.server.hooks;
import spark.Request;
import spark.Response;
import java.util.function.BiFunction;
public class RawAction extends HttpHook.Action {
private final BiFunction<Request, Response, Object> handler;
public RawAction(BiFunction<Request, Response, Object> handler) {
this.handler = handler;
}
@Override
public Object run(Request request, Response response) {
return this.handler.apply(request, response);
}
}

View File

@@ -0,0 +1,43 @@
package eu.mhsl.craftattack.spawn.core.api.server.hooks.impl;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.api.server.hooks.HttpHook;
import eu.mhsl.craftattack.spawn.core.api.server.hooks.JsonAction;
import eu.mhsl.craftattack.spawn.core.event.ReportCreatedEvent;
import eu.mhsl.craftattack.spawn.core.event.SpawnEvent;
import eu.mhsl.craftattack.spawn.core.event.StrikeCreatedEvent;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID;
public class WebsiteHook extends HttpHook {
@Override
protected void registerHooks() {
record CreatedSignup(
String firstname,
String lastname,
String birthday,
@Nullable String telephone,
String username,
String edition,
@Nullable UUID uuid
) {}
this.addAction("signup", new JsonAction<>(CreatedSignup.class, createdSignup -> {
Main.logger().info(String.format("New Website-signup from Hook: %s %s (%s)", createdSignup.firstname, createdSignup.lastname, createdSignup.username));
return HttpServer.nothing;
}));
record CreatedReport(String reporter, String reported, String reason) {}
this.addAction("report", new JsonAction<>(CreatedReport.class, createdReport -> {
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 -> {
SpawnEvent.call(new StrikeCreatedEvent(new StrikeCreatedEvent.CreatedStrike(createdStrike.uuid)));
return HttpServer.nothing;
}));
}
}

View File

@@ -13,6 +13,8 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -23,6 +25,12 @@ import java.util.Optional;
* Appliances can be enabled or disabled independent of other appliances * Appliances can be enabled or disabled independent of other appliances
*/ */
public abstract class Appliance { public abstract class Appliance {
@Retention(RetentionPolicy.RUNTIME)
public @interface Flags {
boolean enabled() default true;
boolean autoload() default true;
}
private String localConfigPath; private String localConfigPath;
private List<Listener> listeners; private List<Listener> listeners;
private List<ApplianceCommand<?>> commands; private List<ApplianceCommand<?>> commands;
@@ -94,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

@@ -0,0 +1,29 @@
package eu.mhsl.craftattack.spawn.core.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class ReportCreatedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public record CreatedReport(String reporter, String reported, String reason) {}
private final CreatedReport report;
public ReportCreatedEvent(CreatedReport report) {
super(true);
this.report = report;
}
public CreatedReport getReport() {
return this.report;
}
}

View File

@@ -0,0 +1,10 @@
package eu.mhsl.craftattack.spawn.core.event;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
public abstract class SpawnEvent {
public static void call(Event event) {
Bukkit.getPluginManager().callEvent(event);
}
}

View File

@@ -0,0 +1,31 @@
package eu.mhsl.craftattack.spawn.core.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class StrikeCreatedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public record CreatedStrike(UUID playerToStrike) {}
private final CreatedStrike strike;
public StrikeCreatedEvent(CreatedStrike strike) {
super(true);
this.strike = strike;
}
public CreatedStrike getStrike() {
return this.strike;
}
}

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

@@ -5,6 +5,7 @@ import eu.mhsl.craftattack.spawn.core.util.statistics.ServerMonitor;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -19,7 +20,12 @@ import java.util.stream.Stream;
public class ComponentUtil { public class ComponentUtil {
public static TextComponent pleaseWait() { public static TextComponent pleaseWait() {
return Component.text("Bitte warte einen Augenblick...", NamedTextColor.GRAY); return Component.text("\uD83D\uDCBE Daten werden geladen... Warte einen Augenblick!", NamedTextColor.GRAY);
}
public static TextComponent clickLink(String url) {
return Component.text("Klicke, um zu öffnen: \uD83D\uDD17[%s]".formatted(url))
.clickEvent(ClickEvent.openUrl(url));
} }
public static Component clearedSpace() { public static Component clearedSpace() {

View File

@@ -2,7 +2,7 @@ dependencies {
implementation project(':core') implementation project(':core')
implementation project(':common') implementation project(':common')
compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4' implementation 'com.sparkjava:spark-core:2.9.4'

View File

@@ -1,13 +1,10 @@
package eu.mhsl.craftattack.spawn.craftattack.api.repositories; package eu.mhsl.craftattack.spawn.craftattack.api.repositories;
import com.google.common.reflect.TypeToken;
import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository; import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
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.CraftAttackApi; import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class FeedbackRepository extends HttpRepository { public class FeedbackRepository extends HttpRepository {
@@ -15,14 +12,15 @@ public class FeedbackRepository extends HttpRepository {
super(CraftAttackApi.getBaseUri(), new RequestModifier(null, CraftAttackApi::withAuthorizationHeader)); super(CraftAttackApi.getBaseUri(), new RequestModifier(null, CraftAttackApi::withAuthorizationHeader));
} }
public record Request(String event, List<UUID> users) { public record Request(String event, String title, List<UUID> users) {
} }
public ReqResp<Map<UUID, String>> createFeedbackUrls(Request data) { public record Response(List<Feedback> feedback) {
final Type responseType = new TypeToken<Map<UUID, String>>() { public record Feedback(UUID uuid, String url) {
}.getType(); }
ReqResp<Object> rawData = this.post("feedback", data, Object.class); }
// TODO: use convertToTypeToken from ReqResp
return new ReqResp<>(rawData.status(), this.gson.fromJson(this.gson.toJson(rawData.data()), responseType)); public ReqResp<Response> createFeedbackUrls(Request data) {
return this.post("feedback", data, Response.class);
} }
} }

View File

@@ -4,6 +4,7 @@ import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
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.CraftAttackApi; import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class WhitelistRepository extends HttpRepository { public class WhitelistRepository extends HttpRepository {
@@ -16,17 +17,15 @@ public class WhitelistRepository extends HttpRepository {
String username, String username,
String firstname, String firstname,
String lastname, String lastname,
Long banned_until, List<Strike> strikes
Long outlawed_until
) { ) {
public record Strike(long at, int weight) {
}
} }
private record UserQuery(UUID uuid) {}
public ReqResp<UserData> getUserData(UUID userId) { public ReqResp<UserData> getUserData(UUID userId) {
return this.post( return this.get(
"player", "users/%s".formatted(userId.toString()),
new UserQuery(userId),
UserData.class UserData.class
); );
} }

View File

@@ -0,0 +1,282 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.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 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.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
@SuppressWarnings("FieldCanBeLocal")
public class Bloodmoon extends Appliance {
public final Map<EntityType, Set<MobEffect>> affectedMobEffectMap = Map.ofEntries(
Map.entry(EntityType.ZOMBIE, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.SKELETON, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.SLOWNESS, 3.5, 1)
)),
Map.entry(EntityType.SPIDER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.POISON, 4, 1),
new MobEffect.PotionMobEffect(PotionEffectType.NAUSEA, 6, 10)
)),
Map.entry(EntityType.CREEPER, Set.of(
new MobEffect.LightningMobEffect()
)),
Map.entry(EntityType.HUSK, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.STRAY, Set.of()),
Map.entry(EntityType.DROWNED, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.WITCH, Set.of()),
Map.entry(EntityType.ZOMBIE_VILLAGER, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.WITHER, 7, 1)
)),
Map.entry(EntityType.PHANTOM, Set.of(
new MobEffect.PotionMobEffect(PotionEffectType.LEVITATION, 1.5, 3)
)),
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 final ThreadLocalRandom random = ThreadLocalRandom.current();
private boolean isActive = false;
private final BossBar bossBar = BossBar.bossBar(
Component.text("Blutmond", NamedTextColor.DARK_RED),
1f,
BossBar.Color.RED,
BossBar.Overlay.NOTCHED_12,
Set.of(BossBar.Flag.CREATE_WORLD_FOG, BossBar.Flag.DARKEN_SCREEN)
);
private final boolean hordesEnabled = true;
private final int hordeSpawnRateTicks = 40 * Ticks.TICKS_PER_SECOND;
private final int hordeSpawnRateVariationTicks = 40 * Ticks.TICKS_PER_SECOND;
private final int hordeMinPopulation = 3;
private final int hordeMaxPopulation = 10;
private final int hordeSpawnDistance = 10;
private final List<EntityType> hordeMobList = List.of(
EntityType.ZOMBIE,
EntityType.SKELETON,
EntityType.SPIDER
);
private final Map<Player, @Nullable BukkitTask> hordeSpawnTasks = new WeakHashMap<>();
private long lastBloodmoonStartTick = 0;
public final int ticksPerDay = 24000;
public final int bloodmoonLength = this.ticksPerDay /2;
public final int preStartMessageTicks = Ticks.TICKS_PER_SECOND * 50;
private final int bloodmoonFreeDaysAtStart = 3;
private final int bloodmoonStartTime = this.ticksPerDay /2;
private final int bloodmoonDayInterval = 30;
private final int minBonusDrops = 1;
private final int maxBonusDrops = 4;
private final Map<ItemStack, Integer> bonusDropWeightMap = Map.of(
new ItemStack(Material.IRON_INGOT, 5), 10,
new ItemStack(Material.GOLD_INGOT, 2), 5,
new ItemStack(Material.DIAMOND, 1), 1,
new ItemStack(Material.IRON_BLOCK, 1), 5,
new ItemStack(Material.GOLD_BLOCK, 1), 2
);
@Override
public void onEnable() {
Settings.instance().declareSetting(BloodmoonSetting.class);
}
public boolean bloodmoonIsActive() {
return this.isActive;
}
public void startBloodmoon(long startTick) {
this.lastBloodmoonStartTick = startTick;
this.isActive = true;
Bukkit.getOnlinePlayers().forEach(this::addPlayerToBossBar);
this.startHordeSpawning(this.getRandomHordeSpawnDelay());
this.sendStartMessages();
}
public void stopBloodmoon() {
this.isActive = false;
Bukkit.getOnlinePlayers().forEach(player -> player.hideBossBar(this.bossBar));
this.sendStopMessages();
}
public void updateBossBar() {
long tick = Bukkit.getWorlds().getFirst().getFullTime();
long sinceStart = tick - this.lastBloodmoonStartTick;
float progress = 1f - ((float) sinceStart / this.bloodmoonLength);
if(progress < 0) progress = 1f;
this.bossBar.progress(progress);
}
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);
return time == this.bloodmoonStartTime;
}
private int getRandomHordeSpawnDelay() {
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) {
@Nullable BukkitTask task = this.hordeSpawnTasks.get(player);
if(task != null) task.cancel();
BukkitTask newTask = Bukkit.getScheduler().runTaskLater(
Main.instance(),
() -> {
if(!this.bloodmoonIsActive()) return;
this.spawnRandomHorde(player);
this.startHordeSpawning(this.getRandomHordeSpawnDelay(), player);
},
delay
);
this.hordeSpawnTasks.put(player, newTask);
}
public void addPlayerToBossBar(Player player) {
if(!this.getBloodmoonSetting(player)) return;
player.showBossBar(this.bossBar);
}
public boolean isAffectedMob(Entity entity) {
return this.affectedMobEffectMap.containsKey(entity.getType());
}
public boolean getBloodmoonSetting(Player player) {
return Settings.instance().getSetting(
player,
Settings.Key.Bloodmoon,
Boolean.class
);
}
public void spawnRandomHorde(Player player) {
if(!this.hordesEnabled) return;
if(!player.getGameMode().equals(GameMode.SURVIVAL)) return;
EntityType hordeEntityType = this.hordeMobList.get(this.random.nextInt(this.hordeMobList.size()));
int hordeSize = this.random.nextInt(this.hordeMinPopulation, this.hordeMaxPopulation + 1);
this.spawnHorde(player, hordeSize, hordeEntityType);
}
public void sendWarningMessage(Player p) {
p.sendMessage(Component.text("Der Blutmond waltet in diesem Augenblick!", NamedTextColor.RED));
}
public void sendAnnouncementMessages() {
this.sendMessage(Component.text("Der Blutmond naht; morgen wird er den Himmel beherrschen", NamedTextColor.GOLD));
}
public void sendPreStartMessages() {
this.sendMessage(Component.text("Der Himmel ist heute Nacht ungewöhnlich düster", NamedTextColor.DARK_PURPLE));
}
private void spawnHorde(Player player, int size, EntityType type) {
for(int i = 0; i < size; i++) {
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().strikeLightningEffect(mobSpawnLocation);
}
}
public List<ItemStack> getRandomBonusDrops() {
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());
}
return result;
}
private @Nullable ItemStack getRandomBonusDrop() {
int totalWeight = this.bonusDropWeightMap.values().stream().mapToInt(value -> value).sum();
int randomInt = this.random.nextInt(0, totalWeight + 1);
int cumulativeWeight = 0;
for(Map.Entry<ItemStack, Integer> entry : this.bonusDropWeightMap.entrySet()) {
cumulativeWeight += entry.getValue();
if(randomInt <= cumulativeWeight) return entry.getKey();
}
return null;
}
private void sendMessage(Component message) {
if(Bukkit.getOnlinePlayers().isEmpty()) return;
List<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers().stream()
.filter(this::getBloodmoonSetting)
.toList();
onlinePlayers.forEach(player -> player.sendMessage(message));
}
private void sendStartMessages() {
this.sendMessage(
Component.empty()
.append(Component.text("Der Blutmond ist über uns", NamedTextColor.DARK_RED, TextDecoration.BOLD))
.appendNewline()
.append(Component.text("Erfahrung vervielfacht sich und Mobs lassen reichere Beute fallen", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Mobs sind erstarkt und belegen dich mit finsteren Debuffs", NamedTextColor.RED))
);
}
private void sendStopMessages() {
this.sendMessage(Component.text("Der Blutmond weicht...", NamedTextColor.GREEN, TextDecoration.BOLD));
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new BloodmoonMonsterDeathListener(),
new BloodmoonPlayerJoinListener(),
new BloodmoonEntityDamageListener(),
new BloodmoonTimeListener()
);
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new BloodmoonCommand()
);
}
}

View File

@@ -0,0 +1,56 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
public class BloodmoonSetting extends BoolSetting implements CategorizedSetting {
public BloodmoonSetting() {
super(Settings.Key.Bloodmoon);
}
@Override
public SettingCategory category() {
return SettingCategory.Misc; // TODO: mehr als 8 bug fixen
}
@Override
protected String title() {
return "Blutmond";
}
@Override
protected String description() {
return "Kämpfe während dem Blutmond gegen stärkere Monster mit besseren Drops. Kann nicht während dem Blutmond geändert werden!";
}
@Override
protected Material icon() {
return Material.SKELETON_SKULL;
}
@Override
protected Boolean defaultValue() {
return true;
}
// TODO: Settings deaktivierbar machen
@Override
protected void change(Player player, ClickType clickType) {
if(Main.instance().getAppliance(Bloodmoon.class).bloodmoonIsActive()) {
InteractSounds.of(player).delete();
player.sendMessage(Component.text("Während dem Blutmond kann diese Einstellung nicht geändert werden!", NamedTextColor.RED));
return;
}
super.change(player, clickType);
}
}

View File

@@ -0,0 +1,29 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon;
import net.kyori.adventure.util.Ticks;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public abstract class MobEffect {
public abstract void apply(Player p);
public static class PotionMobEffect extends MobEffect {
private final PotionEffect effect;
public PotionMobEffect(PotionEffectType type, double seconds, int amplifier) {
this.effect = new PotionEffect(type, (int) (seconds * Ticks.TICKS_PER_SECOND), amplifier);
}
@Override
public void apply(Player p) {
p.addPotionEffect(this.effect);
}
}
public static class LightningMobEffect extends MobEffect {
@Override
public void apply(Player p) {
p.getWorld().strikeLightning(p.getLocation());
}
}
}

View File

@@ -0,0 +1,44 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.commands;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.Bloodmoon;
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 BloodmoonCommand extends ApplianceCommand<Bloodmoon> {
public BloodmoonCommand() {
super("bloodmoon");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if(args.length == 0) {
sender.sendMessage("Bloodmoon is currently %s.".formatted(this.getAppliance().bloodmoonIsActive() ? "active" : "inactive"));
return;
}
switch(args[0]) {
case "start": {
this.getAppliance().startBloodmoon(0L);
sender.sendMessage("Started bloodmoon.");
break;
}
case "stop": {
this.getAppliance().stopBloodmoon();
sender.sendMessage("Stopped bloodmoon.");
break;
}
default: throw new Error("No such option: '%s' !".formatted(args[0]));
}
}
@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("start", "stop");
return null;
}
}

View File

@@ -0,0 +1,35 @@
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 org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class BloodmoonEntityDamageListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
Entity damagerEntity = event.getDamager();
if(damagerEntity instanceof Projectile projectile) {
if(!(projectile.getShooter() instanceof Entity shooter)) return;
damagerEntity = shooter;
}
if(!(damagerEntity instanceof LivingEntity damager && event.getEntity() instanceof LivingEntity receiver)) return;
if(event.getFinalDamage() == 0) return;
if(this.getAppliance().isAffectedMob(damager) && receiver instanceof Player player) {
if(!this.getAppliance().getBloodmoonSetting(player)) return;
event.setDamage(event.getDamage() * this.getAppliance().mobDamageMultiplier);
this.getAppliance().affectedMobEffectMap.get(damager.getType()).forEach(mobEffect -> mobEffect.apply(player));
return;
}
if(this.getAppliance().isAffectedMob(receiver) && damager instanceof Player player) {
if(!this.getAppliance().getBloodmoonSetting(player)) return;
event.setDamage(event.getDamage() / this.getAppliance().mobHealthMultiplier);
}
}
}

View File

@@ -0,0 +1,23 @@
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 org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDeathEvent;
public class BloodmoonMonsterDeathListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onMonsterDeath(EntityDeathEvent event) {
if(!this.getAppliance().bloodmoonIsActive()) return;
if(!(event.getDamageSource().getCausingEntity() instanceof Player player)) return;
if(!this.getAppliance().getBloodmoonSetting(player)) return;
LivingEntity entity = event.getEntity();
if(!this.getAppliance().isAffectedMob(entity)) return;
event.setDroppedExp(event.getDroppedExp() * this.getAppliance().expMultiplier);
event.getDrops().addAll(this.getAppliance().getRandomBonusDrops());
entity.getWorld().strikeLightningEffect(entity.getLocation());
}
}

View File

@@ -0,0 +1,15 @@
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 org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
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());
}
}

View File

@@ -0,0 +1,31 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.bloodmoon.listener;
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;
public class BloodmoonTimeListener extends ApplianceListener<Bloodmoon> {
@EventHandler
public void onServerTick(ServerTickStartEvent event) {
long currentTime = Bukkit.getWorlds().getFirst().getFullTime();
if(this.getAppliance().isStartTick(currentTime)) {
this.getAppliance().startBloodmoon(currentTime);
return;
}
if(this.getAppliance().isStartTick(currentTime - this.getAppliance().bloodmoonLength)) {
this.getAppliance().stopBloodmoon();
return;
}
if(currentTime % Ticks.TICKS_PER_SECOND == 0) this.getAppliance().updateBossBar();
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().ticksPerDay)) {
this.getAppliance().sendAnnouncementMessages();
return;
}
if(this.getAppliance().isStartTick(currentTime + this.getAppliance().preStartMessageTicks)) {
this.getAppliance().sendPreStartMessages();
}
}
}

View File

@@ -51,8 +51,8 @@ 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 Spielern grundlos angegriffen werden."); 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 Spielern 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 FORCED -> Component.text("Du darfst zurzeit deinen Vogelfreistatus nicht ändern, da dieser als Strafe auferlegt wurde!");
}; };
String command = String.format("/%s confirm", OutlawedCommand.commandName); String command = String.format("/%s confirm", OutlawedCommand.commandName);
@@ -74,11 +74,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,11 +126,15 @@ 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("Du darfst von jedem angegriffen und getötet werden!", NamedTextColor.GOLD)); .append(Component.text(
"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
));
}; };
} }
@@ -138,7 +142,9 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
public Component getNamePrefix(Player player) { public Component getNamePrefix(Player player) {
if(this.isOutlawed(player)) { if(this.isOutlawed(player)) {
return Component.text("[☠]", NamedTextColor.RED) return Component.text("[☠]", NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text("Vogelfreie Spieler dürfen ohne Grund angegriffen werden!"))); .hoverEvent(HoverEvent.showText(Component.text(
"PVP-Modus Spieler dürfen von anderen vogelfreien Spielern ohne Grund angegriffen werden!"
)));
} }
return null; return null;

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

@@ -37,6 +37,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 +55,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,18 +85,24 @@ 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) {
@@ -112,18 +124,22 @@ public class Event extends Appliance {
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

@@ -1,7 +1,7 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback; package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback;
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.text.ComponentUtil;
import eu.mhsl.craftattack.spawn.craftattack.api.repositories.FeedbackRepository; import eu.mhsl.craftattack.spawn.craftattack.api.repositories.FeedbackRepository;
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;
@@ -9,7 +9,6 @@ import eu.mhsl.craftattack.spawn.core.api.HttpStatus;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder; import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@@ -18,32 +17,27 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
public class Feedback extends Appliance { public class Feedback extends Appliance {
public Feedback() { public Feedback() {
super("feedback"); super("feedback");
} }
public void requestFeedback(String eventName, List<Player> receivers, @Nullable String question) { public void requestFeedback(String eventName, String title, List<Player> receivers, @Nullable String question) {
ReqResp<Map<UUID, String>> response = this.queryRepository(FeedbackRepository.class).createFeedbackUrls( ReqResp<FeedbackRepository.Response> response = this.queryRepository(FeedbackRepository.class).createFeedbackUrls(
new FeedbackRepository.Request(eventName, receivers.stream().map(Entity::getUniqueId).toList()) new FeedbackRepository.Request(eventName, title, receivers.stream().map(Entity::getUniqueId).toList())
); );
System.out.println(response.toString()); if(response.status() != HttpStatus.OK) throw new RuntimeException();
System.out.println(response.status());
if(response.status() != HttpStatus.CREATED) throw new RuntimeException();
Component border = Component.text("-".repeat(40), NamedTextColor.GRAY); Component border = Component.text("-".repeat(40), NamedTextColor.GRAY);
receivers.forEach(player -> { receivers.forEach(player -> {
String feedbackUrl = response.data().get(player.getUniqueId()); String feedbackUrl = response.data().feedback().stream()
if(feedbackUrl == null) { .filter(feedback -> feedback.uuid().equals(player.getUniqueId()))
Main.logger().warning(String.format("FeedbackUrl not found for player '%s' from backend!", player.getUniqueId())); .findFirst()
return; .orElseThrow()
} .url();
ComponentBuilder<TextComponent, TextComponent.Builder> message = Component.text() ComponentBuilder<TextComponent, TextComponent.Builder> message = Component.text()
.append(border) .append(border)
@@ -58,8 +52,7 @@ public class Feedback extends Appliance {
message message
.append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN) .append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN)
.clickEvent(ClickEvent.openUrl(feedbackUrl))) .hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(feedbackUrl))))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um Feedback zu geben.")))
.appendNewline() .appendNewline()
.append(border); .append(border);

View File

@@ -22,6 +22,7 @@ class FeedbackCommand extends ApplianceCommand.PlayerChecked<Feedback> {
Main.instance(), Main.instance(),
() -> this.getAppliance().requestFeedback( () -> this.getAppliance().requestFeedback(
"self-issued-ingame", "self-issued-ingame",
"Dein Feedback an uns",
List.of(this.getPlayer()), List.of(this.getPlayer()),
null null
) )

View File

@@ -20,6 +20,7 @@ class RequestFeedbackCommand extends ApplianceCommand<Feedback> {
Main.instance(), Main.instance(),
() -> this.getAppliance().requestFeedback( () -> this.getAppliance().requestFeedback(
"admin-issued-ingame", "admin-issued-ingame",
"Hilf uns dein Spielerlebnis zu verbessern!",
new ArrayList<>(Bukkit.getOnlinePlayers()), String.join(" ", args) new ArrayList<>(Bukkit.getOnlinePlayers()), String.join(" ", args)
) )
); );

View File

@@ -0,0 +1,48 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.strikes;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.api.repositories.WhitelistRepository;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public class Strikes extends Appliance {
public Strikes() {
super("strike");
}
private static final BanInfo falseInfo = new BanInfo(false, null, false);
public record BanInfo(boolean isBanned, @Nullable Instant bannedUntil, boolean isPermanent) {
}
private final Map<Integer, Duration> strikePunishmentMap = Map.of(
1, Duration.ofHours(1),
2, Duration.ofHours(24),
3, Duration.ofDays(3),
4, Duration.ofDays(7)
);
public BanInfo isBanned(List<WhitelistRepository.UserData.Strike> strikes) {
if (strikes.isEmpty()) return falseInfo;
int strikeCount = strikes.stream().mapToInt(WhitelistRepository.UserData.Strike::weight).sum();
if (strikeCount < 1) return falseInfo;
if (strikeCount > this.strikePunishmentMap.size()) return new BanInfo(true, null, true);
Instant lastPunishment = strikes.stream()
.map(strike -> Instant.ofEpochMilli(strike.at()))
.max(Instant::compareTo)
.orElse(Instant.EPOCH);
Duration duration = this.strikePunishmentMap.get(strikeCount);
Instant bannedUntil = lastPunishment.plus(duration);
boolean stillBanned = bannedUntil.isAfter(Instant.now());
return new BanInfo(stillBanned, bannedUntil, false);
}
}

View File

@@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.whitelist;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.event.StrikeCreatedEvent;
import org.bukkit.event.EventHandler;
public class StrikeUpdateListener extends ApplianceListener<Whitelist> {
@EventHandler
public void onStrikeUpdate(StrikeCreatedEvent event) {
this.getAppliance().profileUpdated(event.getStrike().playerToStrike());
}
}

View File

@@ -8,21 +8,18 @@ import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.api.HttpStatus; import eu.mhsl.craftattack.spawn.core.api.HttpStatus;
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate; import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo; import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed.Outlawed; import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.strikes.Strikes;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Listener; 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;
import java.time.Instant; import javax.inject.Provider;
import java.time.ZoneOffset; import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.util.*;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class Whitelist extends Appliance { public class Whitelist extends Appliance {
@@ -47,7 +44,6 @@ public class Whitelist extends Appliance {
player.getUniqueId() player.getUniqueId()
); );
} }
this.queryAppliance(Outlawed.class).updateForcedStatus(player, this.timestampRelevant(user.outlawed_until()));
String purePlayerName = Floodgate.isBedrock(player) String purePlayerName = Floodgate.isBedrock(player)
? Floodgate.getBedrockPlayer(player).getUsername() ? Floodgate.getBedrockPlayer(player).getUsername()
@@ -67,23 +63,31 @@ public class Whitelist extends Appliance {
Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name)); Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name));
boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false); boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false);
WhitelistRepository.UserData user = overrideCheck WhitelistRepository.UserData user = overrideCheck
? new WhitelistRepository.UserData(uuid, name, "", "", 0L, 0L) ? new WhitelistRepository.UserData(uuid, name, "", "", List.of())
: this.fetchUserData(uuid); : this.fetchUserData(uuid);
this.userData.put(uuid, user); this.userData.put(uuid, user);
Main.logger().info(String.format("got userdata %s", user.toString())); Main.logger().info(String.format("got userdata %s", user.toString()));
if(this.timestampRelevant(user.banned_until())) { Strikes.BanInfo banInfo = Main.instance().getAppliance(Strikes.class).isBanned(user.strikes());
Instant bannedDate = new Date(user.banned_until() * 1000L) Main.logger().info(String.format("got baninfo %s", banInfo.toString()));
.toInstant()
.plus(1, ChronoUnit.HOURS);
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy").withZone(ZoneOffset.UTC); if (banInfo.isBanned()) {
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneOffset.UTC); Provider<ZonedDateTime> zoned = () -> {
Objects.requireNonNull(banInfo.bannedUntil(), "Cannot get zoned date time from null");
return banInfo.bannedUntil().atZone(ZoneId.of("Europe/Berlin"));
};
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy");
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm");
String unbanText = banInfo.bannedUntil() != null
? String.format("Dein Bann läuft am %s um %s ab!", dateFormat.format(zoned.get()), timeFormat.format(zoned.get()))
: "Bandauer ist unbekannt.";
throw new DisconnectInfo.Throwable( throw new DisconnectInfo.Throwable(
"Du wurdest vom Server gebannt.", "Du wurdest vom Server gebannt.",
String.format("Dein Bann läuft am %s um %s ab!", dateFormat.format(bannedDate), timeFormat.format(bannedDate)), banInfo.isPermanent() ? "Dein Bann läuft nicht ab!" : unbanText,
"Wende dich an einen Admin für weitere Informationen.", "Wende dich an einen Admin für weitere Informationen.",
uuid uuid
); );
@@ -94,18 +98,13 @@ public class Whitelist extends Appliance {
Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage); Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage);
throw new DisconnectInfo.Throwable( throw new DisconnectInfo.Throwable(
"Interner Serverfehler", "Interner Serverfehler",
"Deine Anmeldedaten konnten nicht abgerufen/ überprüft werden.", "Deine Anmeldedaten konnten nicht abgerufen/überprüft werden.",
"Versuche es später erneut oder kontaktiere einen Admin!", "Versuche es später erneut oder kontaktiere einen Admin!",
uuid uuid
); );
} }
} }
private boolean timestampRelevant(Long timestamp) {
if(timestamp == null) return false;
return timestamp > System.currentTimeMillis() / 1000L;
}
private WhitelistRepository.UserData fetchUserData(UUID uuid) throws DisconnectInfo.Throwable { private WhitelistRepository.UserData fetchUserData(UUID uuid) throws DisconnectInfo.Throwable {
ReqResp<WhitelistRepository.UserData> response = this.queryRepository(WhitelistRepository.class).getUserData(uuid); ReqResp<WhitelistRepository.UserData> response = this.queryRepository(WhitelistRepository.class).getUserData(uuid);
@@ -123,20 +122,24 @@ public class Whitelist extends Appliance {
return response.data(); return response.data();
} }
public void profileUpdated(UUID uuid) {
Main.instance().getLogger().info(String.format("API Triggered Profile update for %s", uuid));
Player player = Bukkit.getPlayer(uuid);
if(player != null) {
try {
this.fullIntegrityCheck(player);
} catch(DisconnectInfo.Throwable e) {
e.getDisconnectScreen().applyKick(player);
}
}
}
@Override @Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) { public void httpApi(HttpServer.ApiBuilder apiBuilder) {
record User(UUID user) { record User(UUID user) {
} }
apiBuilder.post("update", User.class, (user, request) -> { apiBuilder.post("update", User.class, (user, request) -> {
Main.instance().getLogger().info(String.format("API Triggered Profile update for %s", user.user)); this.profileUpdated(user.user);
Player player = Bukkit.getPlayer(user.user);
if(player != null) {
try {
this.fullIntegrityCheck(player);
} catch(DisconnectInfo.Throwable e) {
e.getDisconnectScreen().applyKick(player);
}
}
return HttpServer.nothing; return HttpServer.nothing;
}); });
} }
@@ -145,7 +148,8 @@ public class Whitelist extends Appliance {
@NotNull @NotNull
protected List<Listener> listeners() { protected List<Listener> listeners() {
return List.of( return List.of(
new PlayerJoinListener() new PlayerJoinListener(),
new StrikeUpdateListener()
); );
} }
} }

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,151 @@
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.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, 3);
private final double borderDistance = 75;
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("PvP wird in 10 Minuten aktiviert!", NamedTextColor.GOLD));
});
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)));
},
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,152 @@
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.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 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 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,75 @@
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().startEvent(60 * 2);
break;
}
if(args.length == 2) {
try {
this.getAppliance().startEvent(Long.parseLong(args[1]));
} catch(NumberFormatException e) {
throw new Error("Last argument has to be a long.");
}
}
break;
}
case "stop": {
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,157 @@
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.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();
for (int i = 0; i < display.length; i++) {
int idx = display[i];
EventScoreEntry entry = scoreList.get(idx);
String line = this.formattedLine(idx, entry.name(), entry.score());
objective.getScore(line).setScore(display.length - i);
}
return scoreboard;
}
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'

View File

@@ -2,7 +2,7 @@ dependencies {
implementation project(':core') implementation project(':core')
implementation project(':common') implementation project(':common')
compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4' implementation 'com.sparkjava:spark-core:2.9.4'
} }