Compare commits

...

40 Commits

Author SHA1 Message Date
a257b604ea updated InventoryTrackerListener to monitor InventoryOpenEvent with high priority and prevent processing of cancelled events 2025-12-21 09:32:40 +01:00
ac7e04829e introduced ReportCreatedListener, updated ReportCreatedEvent to use UUIDs for reporter and reported, and improved admin notifications for new reports 2025-12-20 17:46:02 +01:00
9767896cde updated OutlawedCommand and Outlawed to rename vogelfrei to PVP, adjusted related messages accordingly 2025-12-20 11:51:30 +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
1ac19014c1 introduced AntiInventoryMove appliance with related listeners and optimized admin notification logic in AcInform 2025-12-13 13:45:37 +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
83 changed files with 1720 additions and 197 deletions

View File

@@ -1,7 +1,7 @@
dependencies {
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'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
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) {
return this.get(
"report",
(parameters) -> parameters.addParameter("uuid", player.toString()),
"users/%s/reports".formatted(player.toString()),
PlayerReports.class
);
}
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post(
"report",
"reports",
data,
ReportUrl.class
);

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,18 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
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.common.appliances.metaGameplay.infoBars.bars.MsptBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.PlayerCounterBar;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsBar;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class InfoBars extends Appliance {
private final NamespacedKey infoBarKey = new NamespacedKey(Main.instance(), "infobars");
private final List<Bar> infoBars = List.of(
new TpsBar(),
new MsptBar(),
@@ -27,41 +20,19 @@ public class InfoBars extends Appliance {
);
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) {
this.getEnabledBars(player).forEach(bar -> this.hide(player, bar));
this.setEnabledBars(player, List.of());
}
public void show(Player player, String bar) {
public void setVisible(Player player, String bar, boolean visible) {
this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getEnabledBars(player));
existingBars.add(bar);
if(visible) {
player.showBossBar(this.getBarByName(bar).getBossBar());
this.setEnabledBars(player, existingBars);
}
public void hide(Player player, String bar) {
this.validateBarName(bar);
List<String> existingBars = new ArrayList<>(this.getEnabledBars(player));
existingBars.remove(bar);
} else {
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) {
@@ -79,13 +50,7 @@ public class InfoBars extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(InfoBarSetting.class);
Settings.instance().addChangeListener(InfoBarSetting.class, player -> {
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);
});
Settings.instance().addChangeListener(InfoBarSetting.class, this::showAllEnabled);
}
@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.core.appliance.Appliance;
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.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
@@ -15,11 +16,14 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
public class Report extends Appliance {
public static Component helpText() {
@@ -78,7 +82,7 @@ public class Report extends Appliance {
.appendNewline()
.append(
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()))
)
.appendNewline()
@@ -128,46 +132,60 @@ public class Report extends Appliance {
return;
}
List<ReportRepository.PlayerReports.Report> reports = userReports
.data()
.from_self()
.stream()
.filter(report -> !report.draft())
.toList()
.reversed();
Function<List<ReportRepository.PlayerReports.Report>, List<ReportRepository.PlayerReports.Report>> filterClosed = reports -> reports.stream()
.filter(report -> Objects.equals(report.status(), ReportRepository.PlayerReports.Report.Status.closed))
.toList();
if(reports.isEmpty()) {
issuer.sendMessage(
Component.text()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY))
);
return;
}
List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed();
List<ReportRepository.PlayerReports.Report> reportsToSelf = filterClosed.apply(userReports.data().to_self()).reversed();
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.newline())
.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD))
.appendNewline();
.append(Component.text(
!reportsToSelf.isEmpty()
? "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
.append(Component.text(" - ", NamedTextColor.WHITE))
.append(
report.reported() != null
? Component.text(report.reported().username(), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW)
)
.append(Component.text(String.format(": %s", report.subject()), NamedTextColor.GRAY))
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um den Report einzusehen.", NamedTextColor.GOLD)));
component.appendNewline();
component.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD));
reportsToOthers.forEach(report -> {
Component button = Component.text("[\uD83D\uDC41/\uD83D\uDD8A]")
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(report.url())));
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());
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new ReportCreatedListener()
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {

View File

@@ -0,0 +1,26 @@
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.core.event.ReportCreatedEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.event.EventHandler;
public class ReportCreatedListener extends ApplianceListener<Report> {
@EventHandler
public void onReport(ReportCreatedEvent event) {
OfflinePlayer reporter = Bukkit.getOfflinePlayer(event.getReport().reporter());
OfflinePlayer reported = Bukkit.getOfflinePlayer(event.getReport().reported());
Component message = Component.text(
"\uD83D\uDD14 Neuer Report von %s gegen %s: %s".formatted(reporter.getName(), reported.getName(), event.getReport().reason()),
NamedTextColor.YELLOW
);
Bukkit.getOnlinePlayers().stream()
.filter(player -> player.hasPermission("admin"))
.forEach(player -> player.sendMessage(message));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import net.kyori.adventure.util.Ticks;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Appliance.Flags(enabled = false)
public class AntiInventoryMove extends Appliance {
private static final long errorTimeMargin = Ticks.SINGLE_TICK_DURATION_MS * 2;
private final Map<UUID, Long> invOpen = new ConcurrentHashMap<>();
public void setInvOpen(Player player, boolean open) {
if(open)
this.invOpen.put(player.getUniqueId(), System.currentTimeMillis());
else
this.invOpen.remove(player.getUniqueId());
}
public boolean hasInventoryOpen(Player player) {
if(!this.invOpen.containsKey(player.getUniqueId())) return false;
return this.invOpen.get(player.getUniqueId()) < System.currentTimeMillis() - errorTimeMargin;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new InventoryTrackerListener(),
new InInventoryMoveListener()
);
}
}

View File

@@ -0,0 +1,21 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInputEvent;
class InInventoryMoveListener extends ApplianceListener<AntiInventoryMove> {
@EventHandler
public void onInput(PlayerInputEvent event) {
if(!this.getAppliance().hasInventoryOpen(event.getPlayer())) return;
Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
"internal",
event.getPlayer().getName(),
"inInventoryMove",
-1f,
3000
);
}
}

View File

@@ -0,0 +1,23 @@
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
class InventoryTrackerListener extends ApplianceListener<AntiInventoryMove> {
@EventHandler(priority = EventPriority.MONITOR)
public void onOpen(InventoryOpenEvent event) {
if(!(event.getPlayer() instanceof Player player)) return;
if(event.isCancelled()) return;
this.getAppliance().setInvOpen(player, true);
}
@EventHandler
public void onClose(InventoryCloseEvent event) {
if(!(event.getPlayer() instanceof Player player)) return;
this.getAppliance().setInvOpen(player, false);
}
}

View File

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

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;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.core.util.entity.PlayerUtils;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,9 +30,36 @@ public class Kick extends Appliance {
).applyKick(player);
}
public void unsuspectedKick(@NotNull String playerName) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
String material = Material.values()[(int)(Math.random() * Material.values().length)].name();
player.kick(Component.text("java.lang.IllegalStateException: Failed to create model for minecraft:%s".formatted(material)));
}
public void crashKick(@NotNull String playerName) {
Player player = Bukkit.getPlayer(playerName);
if(player == null)
throw new ApplianceCommand.Error("Player not found");
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
PlayerUtils.sendCube(player, 100, Material.ENCHANTING_TABLE.createBlockData());
PlayerUtils.sendCube(player, 5, Material.DIRT.createBlockData());
});
Bukkit.getScheduler().runTaskLater(Main.instance(), () -> player.kick(Component.empty()), Ticks.TICKS_PER_SECOND * 15);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new KickCommand());
return List.of(
new KickCommand(),
new KickUnsuspectedCommand(),
new KickCrashCommand()
);
}
}

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 {
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'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4'

View File

@@ -51,6 +51,11 @@ public final class Main extends JavaPlugin {
Main.logger().info("Loading appliances...");
this.appliances = this.findSubtypesOf(Appliance.class).stream()
.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 -> {
try {
return (Appliance) applianceClass.getDeclaredConstructor().newInstance();

View File

@@ -2,11 +2,13 @@ package eu.mhsl.craftattack.spawn.core.api.server;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.hooks.HttpHook;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.configuration.ConfigurationSection;
import spark.Request;
import spark.Spark;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -21,6 +23,11 @@ public class HttpServer {
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)));
}
@@ -43,12 +50,16 @@ public class HttpServer {
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) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public void rawPost(String path, Function<Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
public void rawPost(String path, BiFunction<Request, spark.Response, Object> onCall) {
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) {
@@ -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);
}

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,45 @@
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(UUID reporter, UUID reported, String reason) {}
this.addAction("report", new JsonAction<>(CreatedReport.class, createdReport -> {
Main.logger().info(String.format("New Report from Hook: (%s) Reporter: %s Reported: %s", createdReport.reason, createdReport.reporter, createdReport.reported));
SpawnEvent.call(new ReportCreatedEvent(new ReportCreatedEvent.CreatedReport(createdReport.reporter, createdReport.reported, createdReport.reason)));
return HttpServer.nothing;
}));
record CreatedStrike(UUID uuid) {}
this.addAction("strike", new JsonAction<>(CreatedStrike.class, createdStrike -> {
Main.logger().info(String.format("New Strike from Hook! (User %s)", createdStrike.uuid));
SpawnEvent.call(new StrikeCreatedEvent(new StrikeCreatedEvent.CreatedStrike(createdStrike.uuid)));
return HttpServer.nothing;
}));
}
}

View File

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

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 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(UUID reporter, UUID 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;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class PlayerUtils {
public static void resetStatistics(Player player) {
for(Statistic statistic : Statistic.values()) {
@@ -30,4 +36,30 @@ public class PlayerUtils {
}
}
}
public static void sendCube(Player player, int cubeSize, BlockData fakeBlock) {
Location loc = player.getLocation();
World world = player.getWorld();
int half = cubeSize / 2;
int cx = loc.getBlockX();
int cy = loc.getBlockY();
int cz = loc.getBlockZ();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight() - 1;
Map<Location, BlockData> changes = new HashMap<>();
for (int x = cx - half; x <= cx + half; x++) {
for (int y = Math.max(cy - half, minY); y <= Math.min(cy + half, maxY); y++) {
for (int z = cz - half; z <= cz + half; z++) {
changes.put(new Location(world, x, y, z), fakeBlock);
}
}
}
//noinspection UnstableApiUsage
player.sendMultiBlockChange(changes);
}
}

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.ComponentBuilder;
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.TextColor;
import org.bukkit.Bukkit;
@@ -19,7 +20,12 @@ import java.util.stream.Stream;
public class ComponentUtil {
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() {

View File

@@ -2,7 +2,7 @@ dependencies {
implementation project(':core')
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'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4'

View File

@@ -1,13 +1,10 @@
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.ReqResp;
import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class FeedbackRepository extends HttpRepository {
@@ -15,14 +12,15 @@ public class FeedbackRepository extends HttpRepository {
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) {
final Type responseType = new TypeToken<Map<UUID, String>>() {
}.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 record Response(List<Feedback> feedback) {
public record Feedback(UUID uuid, String url) {
}
}
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.common.api.CraftAttackApi;
import java.util.List;
import java.util.UUID;
public class WhitelistRepository extends HttpRepository {
@@ -16,17 +17,15 @@ public class WhitelistRepository extends HttpRepository {
String username,
String firstname,
String lastname,
Long banned_until,
Long outlawed_until
List<Strike> strikes
) {
public record Strike(long at, int weight) {
}
}
private record UserQuery(UUID uuid) {}
public ReqResp<UserData> getUserData(UUID userId) {
return this.post(
"player",
new UserQuery(userId),
return this.get(
"users/%s".formatted(userId.toString()),
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 = ticksPerDay/2;
public final int preStartMessageTicks = Ticks.TICKS_PER_SECOND * 50;
private final int bloodmoonFreeDaysAtStart = 3;
private final int bloodmoonStartTime = 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) {
Component confirmationMessage = switch(this.getLawStatus(player)) {
case DISABLED -> Component.text("Wenn du Vogelfrei aktivierst, darfst du von allen Spielern grundlos angegriffen werden.");
case VOLUNTARILY -> Component.text("Wenn du Vogelfrei deaktivierst, darfst du nicht mehr grundlos von Spielern 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 anderen Spielern angegriffen werden.");
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);
@@ -74,11 +74,11 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
void switchLawStatus(Player player) throws OutlawChangeNotPermitted {
if(this.getLawStatus(player).equals(Status.FORCED)) {
throw new OutlawChangeNotPermitted("Dein Vogelfreistatus wurde als Strafe auferlegt und kann daher nicht verändert werden.");
throw new OutlawChangeNotPermitted("Dein PVP-Status wurde als Strafe auferlegt und kann daher nicht verändert werden.");
}
if(this.isTimeout(player)) {
throw new OutlawChangeNotPermitted("Du kannst deinen Vogelfreistatus nicht so schnell wechseln. Bitte warte einige Stunden bevor du umschaltest!");
throw new OutlawChangeNotPermitted("Du kannst deinen PVP-Status nicht so schnell wechseln. Bitte warte einige Stunden bevor du umschaltest!");
}
this.setLawStatus(player, this.isOutlawed(player) ? Status.DISABLED : Status.VOLUNTARILY);
@@ -126,11 +126,15 @@ public class Outlawed extends Appliance implements DisplayName.Prefixed {
public Component getStatusDescription(Status status) {
return switch(status) {
case DISABLED -> Component.text("Vogelfreistatus inaktiv: ", NamedTextColor.GREEN)
case DISABLED -> Component.text("PVP-Modus inaktiv: ", NamedTextColor.GREEN)
.append(Component.text("Es gelten die normalen Regeln!", NamedTextColor.GOLD));
case VOLUNTARILY, FORCED -> Component.text("Vogelfreistatus aktiv: ", NamedTextColor.RED)
.append(Component.text("Du darfst von jedem angegriffen und getötet werden!", NamedTextColor.GOLD));
case VOLUNTARILY, FORCED -> Component.text("PVP-Modus aktiv: ", NamedTextColor.RED)
.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) {
if(this.isOutlawed(player)) {
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;

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ class RequestFeedbackCommand extends ApplianceCommand<Feedback> {
Main.instance(),
() -> this.getAppliance().requestFeedback(
"admin-issued-ingame",
"Hilf uns dein Spielerlebnis zu verbessern!",
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.util.server.Floodgate;
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.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.time.ZoneOffset;
import javax.inject.Provider;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.*;
import java.util.logging.Level;
public class Whitelist extends Appliance {
@@ -47,7 +44,6 @@ public class Whitelist extends Appliance {
player.getUniqueId()
);
}
this.queryAppliance(Outlawed.class).updateForcedStatus(player, this.timestampRelevant(user.outlawed_until()));
String purePlayerName = Floodgate.isBedrock(player)
? Floodgate.getBedrockPlayer(player).getUsername()
@@ -67,23 +63,31 @@ public class Whitelist extends Appliance {
Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name));
boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false);
WhitelistRepository.UserData user = overrideCheck
? new WhitelistRepository.UserData(uuid, name, "", "", 0L, 0L)
? new WhitelistRepository.UserData(uuid, name, "", "", List.of())
: this.fetchUserData(uuid);
this.userData.put(uuid, user);
Main.logger().info(String.format("got userdata %s", user.toString()));
if(this.timestampRelevant(user.banned_until())) {
Instant bannedDate = new Date(user.banned_until() * 1000L)
.toInstant()
.plus(1, ChronoUnit.HOURS);
Strikes.BanInfo banInfo = Main.instance().getAppliance(Strikes.class).isBanned(user.strikes());
Main.logger().info(String.format("got baninfo %s", banInfo.toString()));
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy").withZone(ZoneOffset.UTC);
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneOffset.UTC);
if (banInfo.isBanned()) {
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(
"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.",
uuid
);
@@ -101,11 +105,6 @@ public class Whitelist extends Appliance {
}
}
private boolean timestampRelevant(Long timestamp) {
if(timestamp == null) return false;
return timestamp > System.currentTimeMillis() / 1000L;
}
private WhitelistRepository.UserData fetchUserData(UUID uuid) throws DisconnectInfo.Throwable {
ReqResp<WhitelistRepository.UserData> response = this.queryRepository(WhitelistRepository.class).getUserData(uuid);
@@ -123,13 +122,9 @@ public class Whitelist extends Appliance {
return response.data();
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
record User(UUID user) {
}
apiBuilder.post("update", User.class, (user, request) -> {
Main.instance().getLogger().info(String.format("API Triggered Profile update for %s", user.user));
Player player = Bukkit.getPlayer(user.user);
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);
@@ -137,6 +132,14 @@ public class Whitelist extends Appliance {
e.getDisconnectScreen().applyKick(player);
}
}
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
record User(UUID user) {
}
apiBuilder.post("update", User.class, (user, request) -> {
this.profileUpdated(user.user);
return HttpServer.nothing;
});
}
@@ -145,7 +148,8 @@ public class Whitelist extends Appliance {
@NotNull
protected List<Listener> listeners() {
return List.of(
new PlayerJoinListener()
new PlayerJoinListener(),
new StrikeUpdateListener()
);
}
}

View File

@@ -2,7 +2,7 @@ dependencies {
implementation project(':core')
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 'com.sparkjava:spark-core:2.9.4'
}