Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/main/java/pro/cloudnode/smp/smpcore/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Objects;

public final class Configuration extends BaseConfig {
Expand Down Expand Up @@ -42,6 +44,24 @@ public int joinRequestExpireMinutes() {
return config.getInt("join.request-expire-minutes");
}

/**
* Ban players upon death
*/
public boolean deathBanEnabled() {
return config.getBoolean("death-ban.enabled");
}

public @NotNull Component deathBanMessage() {
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("death-ban.message")));
}

public @NotNull Duration deathBanProgression(final int index) {
List<Duration> progression = config.getStringList("death-ban.progression")
.stream().map(Duration::parse).toList();
if (index >= progression.size()) progression.getLast();
return progression.get(index);
}

public @NotNull Component relativeTime(final Number t, final @NotNull ChronoUnit unit) {
final @NotNull String formatString = Objects.requireNonNull(config.getString("relative-time." + switch (unit) {
case SECONDS -> "seconds";
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/pro/cloudnode/smp/smpcore/Messages.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package pro.cloudnode.smp.smpcore;

import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Formatter;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.bukkit.BanEntry;
import org.bukkit.OfflinePlayer;
import org.bukkit.permissions.Permissible;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -463,6 +467,23 @@ public Messages() {
);
}

public @NotNull Optional<@NotNull Component> banScreen(final @NotNull BanEntry<PlayerProfile> banEntry) {
final @Nullable Date expiration = banEntry.getExpiration();
final @Nullable String template = config.getString("ban-screen." + (expiration == null ? "permanent" : "temporary"));
if (template == null || template.isBlank() || template.equals("null"))
return Optional.empty();

List<TagResolver> placeholders = new ArrayList<>();
placeholders.add(Placeholder.unparsed("reason", Optional.ofNullable(banEntry.getReason()).orElse("")));
if (expiration != null) {
final @NotNull ZonedDateTime localExpiry = expiration.toInstant().atZone(ZoneOffset.systemDefault());
placeholders.add(Formatter.date("expiration", localExpiry));
placeholders.add(Placeholder.component("expiration-relative", SMPCore.relativeTime(expiration)));
}

return Optional.of(MiniMessage.miniMessage().deserialize(template, placeholders.toArray(TagResolver[]::new)));
}

// errors

public @NotNull Component errorNoPermission() {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/pro/cloudnode/smp/smpcore/Permission.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,9 @@ public final class Permission {
* Relieve vice-leader of any nation of their duties
*/
public static @NotNull String NATION_DEMOTE_OTHER = "smpcore.nation.citizens.demote.other";

/**
* Bypass death ban.
*/
public static final @NotNull String DEATHBAN_BYPASS = "smpcore.deathban.bypass";
}
6 changes: 6 additions & 0 deletions src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
import pro.cloudnode.smp.smpcore.command.TimeCommand;
import pro.cloudnode.smp.smpcore.command.UnbanCommand;
import pro.cloudnode.smp.smpcore.listener.NationTeamUpdaterListener;
import pro.cloudnode.smp.smpcore.listener.PlayerDeathListener;
import pro.cloudnode.smp.smpcore.listener.PlayerPostRespawnListener;
import pro.cloudnode.smp.smpcore.listener.PlayerSlotsListener;
import pro.cloudnode.smp.smpcore.listener.PlayerPreLoginListener;

import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -73,6 +76,9 @@ public void onEnable() {

getServer().getPluginManager().registerEvents(new NationTeamUpdaterListener(), this);
getServer().getPluginManager().registerEvents(new PlayerSlotsListener(), this);
getServer().getPluginManager().registerEvents(new PlayerDeathListener(), this);
getServer().getPluginManager().registerEvents(new PlayerPreLoginListener(), this);
getServer().getPluginManager().registerEvents(new PlayerPostRespawnListener(), this);

final @NotNull HashMap<@NotNull String, @NotNull Command> commands = new HashMap<>() {{
put("smpcore", new MainCommand());
Expand Down
98 changes: 73 additions & 25 deletions src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import pro.cloudnode.smp.smpcore.Member;
Expand All @@ -13,14 +12,70 @@
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public final class BanCommand extends Command {
private static void ban(
final @NotNull OfflinePlayer player,
final @Nullable String reason,
final @Nullable Date expiry,
final @Nullable String source
) {
SMPCore.runMain(() -> player.ban(reason, expiry, source));
}

/**
* Bans a player and all their alts.
*
* @return List of all banned members, with the main account as the first element.
* <ul>
* <li>Empty list: the banned player was not a member.</li>
* <li>One element: the banned member had no alts.</li>
* <li>Multiple elements: the main account, followed by their alts.</li>
* </ul>
*/
public static @NotNull List<@NotNull Member> ban(
final @NotNull OfflinePlayer player,
final @Nullable String reason,
final @Nullable Duration duration,
final @Nullable OfflinePlayer source
) {
final String banSource = new NamespacedKey(
SMPCore.getInstance(),
source == null ? "console" : "player/" + source.getUniqueId()
).asString();

final @Nullable Date banExpiry = duration == null
? null
: Date.from(Instant.now().plus(duration));

final Optional<Member> targetMember = Member.get(player);
if (targetMember.isEmpty()) {
ban(player, reason, banExpiry, banSource);
return List.of();
}

final Member main = targetMember.get().altOwner().orElse(targetMember.get());
final Set<Member> alts = main.getAlts();

ban(main.player(), reason, banExpiry, banSource);
final List<Member> bannedMembers = new ArrayList<>();
bannedMembers.add(main);
if (alts.isEmpty()) return bannedMembers;

for (final Member alt : alts) {
ban(alt.player(), reason, banExpiry, banSource);
bannedMembers.add(alt);
}
return bannedMembers;
}

/**
* Usage: {@code /<command> <username> [duration] [reason]}
*/
Expand All @@ -33,7 +88,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul

final @Nullable String durationArg = args.length > 1 ? args[1] : null;
@Nullable Duration duration = null;
if (durationArg != null && durationArg.matches("(?i)^PT\\d.*")) try {
if (durationArg != null && durationArg.matches("(?i)^PT?\\d.*")) try {
duration = Duration.parse(durationArg);
}
catch (DateTimeParseException ignored) {
Expand All @@ -43,34 +98,27 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul
if (duration != null && (duration.isNegative() || duration.isZero()))
return sendMessage(sender, SMPCore.messages().errorDurationZeroOrLess());

final @Nullable Date banExpiry = duration == null ? null : Date.from(Instant.now().plus(duration));

final @Nullable String reason = args.length > 1
? String.join(" ", Arrays.copyOfRange(args, duration == null ? 1 : 2, args.length))
: null;
final @NotNull NamespacedKey banSource;
if (sender instanceof final @NotNull Player player)
banSource = new NamespacedKey(SMPCore.getInstance(), "player/" + player.getUniqueId());
else banSource = new NamespacedKey(SMPCore.getInstance(), "console");

final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]);
final @NotNull Optional<@NotNull Member> targetMember = Member.get(target);
if (targetMember.isEmpty()) {
SMPCore.runMain(() -> target.ban(reason, banExpiry, banSource.asString()));

final List<Member> banned = ban(
target,
reason,
duration,
sender instanceof final OfflinePlayer player ? player : null
);

if (banned.isEmpty())
return sendMessage(sender, SMPCore.messages().bannedPlayer(target, duration));
}
final @NotNull Member main = targetMember.get().altOwner().orElse(targetMember.get());
final @NotNull HashSet<@NotNull Member> alts = main.getAlts();

SMPCore.runMain(() -> main.player().ban(reason, banExpiry, banSource.asString()));
if (alts.isEmpty()) return sendMessage(sender, SMPCore.messages().bannedMember(main, duration));
else {
SMPCore.runMain(() -> {
for (final @NotNull Member alt : alts)
alt.player().ban(reason, banExpiry, banSource.asString());
});
return sendMessage(sender, SMPCore.messages().bannedMemberChain(main, alts.stream().toList(), duration));
}
if (banned.size() == 1)
return sendMessage(sender, SMPCore.messages().bannedMember(banned.get(0), duration));
return sendMessage(sender, SMPCore.messages().bannedMemberChain(
banned.get(0),
banned.subList(1, banned.size()), duration)
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package pro.cloudnode.smp.smpcore.listener;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Statistic;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.jetbrains.annotations.NotNull;
import pro.cloudnode.smp.smpcore.Permission;
import pro.cloudnode.smp.smpcore.SMPCore;
import pro.cloudnode.smp.smpcore.command.BanCommand;

import java.util.Optional;

public final class PlayerDeathListener implements Listener {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void banPlayer(final @NotNull PlayerDeathEvent event) {
if (!SMPCore.config().deathBanEnabled())
return;

final @NotNull Player player = event.getPlayer();

if (player.hasPermission(Permission.DEATHBAN_BYPASS))
return;

player.spigot().respawn();
player.setGameMode(player.getServer().getDefaultGameMode());

final Component reason = Optional.ofNullable(event.deathMessage())
.orElse(SMPCore.config().deathBanMessage());

BanCommand.ban(
player,
PlainTextComponentSerializer.plainText().serialize(reason),
SMPCore.config().deathBanProgression(player.getStatistic(Statistic.DEATHS)),
null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package pro.cloudnode.smp.smpcore.listener;

import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;

public final class PlayerPostRespawnListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void enforceDefaultGamemode(final @NotNull PlayerPostRespawnEvent event) {
final Player player = event.getPlayer();

if (player.hasPermission("minecraft.command.gamemode"))
return;

player.setGameMode(player.getServer().getDefaultGameMode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pro.cloudnode.smp.smpcore.listener;

import com.destroystokyo.paper.profile.PlayerProfile;
import io.papermc.paper.ban.BanListType;
import org.bukkit.BanEntry;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import pro.cloudnode.smp.smpcore.SMPCore;

public final class PlayerPreLoginListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void formatBanScreen(final @NotNull AsyncPlayerPreLoginEvent event) {
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED)
return;

@Nullable BanEntry<PlayerProfile> banEntry = SMPCore.getInstance().getServer()
.getBanList(BanListType.PROFILE).getBanEntry(event.getPlayerProfile());

if (banEntry == null)
return;

SMPCore.messages().banScreen(banEntry)
.ifPresent(reason -> event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, reason));
}
}
22 changes: 22 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ join:
# The number of minutes after which requests/invitations to join a nation expire
request-expire-minutes: 1440

# Ban players upon death
death-ban:
enabled: true
message: <lang:deathScreen.title>

# Ban durations
progression:
- PT1M
- PT2M
- PT5M
- PT10M
- PT20M
- PT45M
- PT1H30M
- PT3H
- PT6H
- PT12H
- P1D
- P3D
- P7D
- P14D

relative-time:
seconds: <t> <format:'0#seconds|1#second|1<seconds'>
minutes: <t> <format:'0#minutes|1#minute|1<minutes'>
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ nation:
request-rejected: <green>(!) You have rejected the request of <gray><player></gray> to join <gray><nation></gray>.</green>
invite-cancelled: <green>(!) You have cancelled the invitation to <gray><player></gray> to join <gray><nation></gray>.</green>
invite-rejected: <green>(!) You have rejected the invitation to join <gray><nation></gray>.</green>

ban-screen:
permanent: |-
<red><lang:multiplayer.disconnect.banned></red>

<reason>

temporary: |-
<red><lang:multiplayer.disconnect.banned></red>

<reason>
<gray><lang:multiplayer.disconnect.banned.expiration:'<white><expiration:"yyyy-MM-dd"></white> at <white><expiration:"HH:mm:ss z"></white>'> (<expiration-relative>).</gray>

error:
no-permission: <red>(!) You don't have permission to use this command.</red>
player-not-banned: <red>(!) Player <gray><player></gray> is not banned and is not a member.</red>
Expand Down
Loading