Skip to content

Commit a63d974

Browse files
authored
Added death ban (#54)
2 parents 9335b22 + 488e6fc commit a63d974

File tree

10 files changed

+251
-25
lines changed

10 files changed

+251
-25
lines changed

src/main/java/pro/cloudnode/smp/smpcore/Configuration.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
77
import org.jetbrains.annotations.NotNull;
88

9+
import java.time.Duration;
910
import java.time.temporal.ChronoUnit;
11+
import java.util.List;
1012
import java.util.Objects;
1113

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

47+
/**
48+
* Ban players upon death
49+
*/
50+
public boolean deathBanEnabled() {
51+
return config.getBoolean("death-ban.enabled");
52+
}
53+
54+
public @NotNull Component deathBanMessage() {
55+
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("death-ban.message")));
56+
}
57+
58+
public @NotNull Duration deathBanProgression(final int index) {
59+
List<Duration> progression = config.getStringList("death-ban.progression")
60+
.stream().map(Duration::parse).toList();
61+
if (index >= progression.size()) progression.getLast();
62+
return progression.get(index);
63+
}
64+
4565
public @NotNull Component relativeTime(final Number t, final @NotNull ChronoUnit unit) {
4666
final @NotNull String formatString = Objects.requireNonNull(config.getString("relative-time." + switch (unit) {
4767
case SECONDS -> "seconds";

src/main/java/pro/cloudnode/smp/smpcore/Messages.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
package pro.cloudnode.smp.smpcore;
22

3+
import com.destroystokyo.paper.profile.PlayerProfile;
34
import net.kyori.adventure.text.Component;
45
import net.kyori.adventure.text.JoinConfiguration;
56
import net.kyori.adventure.text.TextComponent;
67
import net.kyori.adventure.text.minimessage.MiniMessage;
78
import net.kyori.adventure.text.minimessage.tag.resolver.Formatter;
89
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
10+
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
11+
import org.bukkit.BanEntry;
912
import org.bukkit.OfflinePlayer;
1013
import org.bukkit.permissions.Permissible;
1114
import org.jetbrains.annotations.NotNull;
1215
import org.jetbrains.annotations.Nullable;
1316

1417
import java.time.Duration;
1518
import java.time.ZoneOffset;
19+
import java.time.ZonedDateTime;
1620
import java.time.temporal.ChronoUnit;
1721
import java.util.ArrayList;
1822
import java.util.Arrays;
@@ -463,6 +467,23 @@ public Messages() {
463467
);
464468
}
465469

470+
public @NotNull Optional<@NotNull Component> banScreen(final @NotNull BanEntry<PlayerProfile> banEntry) {
471+
final @Nullable Date expiration = banEntry.getExpiration();
472+
final @Nullable String template = config.getString("ban-screen." + (expiration == null ? "permanent" : "temporary"));
473+
if (template == null || template.isBlank() || template.equals("null"))
474+
return Optional.empty();
475+
476+
List<TagResolver> placeholders = new ArrayList<>();
477+
placeholders.add(Placeholder.unparsed("reason", Optional.ofNullable(banEntry.getReason()).orElse("")));
478+
if (expiration != null) {
479+
final @NotNull ZonedDateTime localExpiry = expiration.toInstant().atZone(ZoneOffset.systemDefault());
480+
placeholders.add(Formatter.date("expiration", localExpiry));
481+
placeholders.add(Placeholder.component("expiration-relative", SMPCore.relativeTime(expiration)));
482+
}
483+
484+
return Optional.of(MiniMessage.miniMessage().deserialize(template, placeholders.toArray(TagResolver[]::new)));
485+
}
486+
466487
// errors
467488

468489
public @NotNull Component errorNoPermission() {

src/main/java/pro/cloudnode/smp/smpcore/Permission.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,9 @@ public final class Permission {
156156
* Relieve vice-leader of any nation of their duties
157157
*/
158158
public static @NotNull String NATION_DEMOTE_OTHER = "smpcore.nation.citizens.demote.other";
159+
160+
/**
161+
* Bypass death ban.
162+
*/
163+
public static final @NotNull String DEATHBAN_BYPASS = "smpcore.deathban.bypass";
159164
}

src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
import pro.cloudnode.smp.smpcore.command.TimeCommand;
1818
import pro.cloudnode.smp.smpcore.command.UnbanCommand;
1919
import pro.cloudnode.smp.smpcore.listener.NationTeamUpdaterListener;
20+
import pro.cloudnode.smp.smpcore.listener.PlayerDeathListener;
21+
import pro.cloudnode.smp.smpcore.listener.PlayerPostRespawnListener;
2022
import pro.cloudnode.smp.smpcore.listener.PlayerSlotsListener;
23+
import pro.cloudnode.smp.smpcore.listener.PlayerPreLoginListener;
2124

2225
import java.io.IOException;
2326
import java.io.InputStream;
@@ -73,6 +76,9 @@ public void onEnable() {
7376

7477
getServer().getPluginManager().registerEvents(new NationTeamUpdaterListener(), this);
7578
getServer().getPluginManager().registerEvents(new PlayerSlotsListener(), this);
79+
getServer().getPluginManager().registerEvents(new PlayerDeathListener(), this);
80+
getServer().getPluginManager().registerEvents(new PlayerPreLoginListener(), this);
81+
getServer().getPluginManager().registerEvents(new PlayerPostRespawnListener(), this);
7682

7783
final @NotNull HashMap<@NotNull String, @NotNull Command> commands = new HashMap<>() {{
7884
put("smpcore", new MainCommand());

src/main/java/pro/cloudnode/smp/smpcore/command/BanCommand.java

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import org.bukkit.NamespacedKey;
44
import org.bukkit.OfflinePlayer;
55
import org.bukkit.command.CommandSender;
6-
import org.bukkit.entity.Player;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
98
import pro.cloudnode.smp.smpcore.Member;
@@ -13,14 +12,70 @@
1312
import java.time.Duration;
1413
import java.time.Instant;
1514
import java.time.format.DateTimeParseException;
15+
import java.util.ArrayList;
1616
import java.util.Arrays;
1717
import java.util.Date;
18-
import java.util.HashSet;
1918
import java.util.List;
2019
import java.util.Objects;
2120
import java.util.Optional;
21+
import java.util.Set;
2222

2323
public final class BanCommand extends Command {
24+
private static void ban(
25+
final @NotNull OfflinePlayer player,
26+
final @Nullable String reason,
27+
final @Nullable Date expiry,
28+
final @Nullable String source
29+
) {
30+
SMPCore.runMain(() -> player.ban(reason, expiry, source));
31+
}
32+
33+
/**
34+
* Bans a player and all their alts.
35+
*
36+
* @return List of all banned members, with the main account as the first element.
37+
* <ul>
38+
* <li>Empty list: the banned player was not a member.</li>
39+
* <li>One element: the banned member had no alts.</li>
40+
* <li>Multiple elements: the main account, followed by their alts.</li>
41+
* </ul>
42+
*/
43+
public static @NotNull List<@NotNull Member> ban(
44+
final @NotNull OfflinePlayer player,
45+
final @Nullable String reason,
46+
final @Nullable Duration duration,
47+
final @Nullable OfflinePlayer source
48+
) {
49+
final String banSource = new NamespacedKey(
50+
SMPCore.getInstance(),
51+
source == null ? "console" : "player/" + source.getUniqueId()
52+
).asString();
53+
54+
final @Nullable Date banExpiry = duration == null
55+
? null
56+
: Date.from(Instant.now().plus(duration));
57+
58+
final Optional<Member> targetMember = Member.get(player);
59+
if (targetMember.isEmpty()) {
60+
ban(player, reason, banExpiry, banSource);
61+
return List.of();
62+
}
63+
64+
final Member main = targetMember.get().altOwner().orElse(targetMember.get());
65+
final Set<Member> alts = main.getAlts();
66+
67+
ban(main.player(), reason, banExpiry, banSource);
68+
final List<Member> bannedMembers = new ArrayList<>();
69+
bannedMembers.add(main);
70+
if (alts.isEmpty()) return bannedMembers;
71+
72+
for (final Member alt : alts) {
73+
ban(alt.player(), reason, banExpiry, banSource);
74+
bannedMembers.add(alt);
75+
}
76+
return bannedMembers;
77+
}
78+
2479
/**
2580
* Usage: {@code /<command> <username> [duration] [reason]}
2681
*/
@@ -33,7 +88,7 @@ public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNul
3388

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

46-
final @Nullable Date banExpiry = duration == null ? null : Date.from(Instant.now().plus(duration));
47-
48101
final @Nullable String reason = args.length > 1
49102
? String.join(" ", Arrays.copyOfRange(args, duration == null ? 1 : 2, args.length))
50103
: null;
51-
final @NotNull NamespacedKey banSource;
52-
if (sender instanceof final @NotNull Player player)
53-
banSource = new NamespacedKey(SMPCore.getInstance(), "player/" + player.getUniqueId());
54-
else banSource = new NamespacedKey(SMPCore.getInstance(), "console");
55104

56105
final @NotNull OfflinePlayer target = SMPCore.getInstance().getServer().getOfflinePlayer(args[0]);
57-
final @NotNull Optional<@NotNull Member> targetMember = Member.get(target);
58-
if (targetMember.isEmpty()) {
59-
SMPCore.runMain(() -> target.ban(reason, banExpiry, banSource.asString()));
106+
107+
final List<Member> banned = ban(
108+
target,
109+
reason,
110+
duration,
111+
sender instanceof final OfflinePlayer player ? player : null
112+
);
113+
114+
if (banned.isEmpty())
60115
return sendMessage(sender, SMPCore.messages().bannedPlayer(target, duration));
61-
}
62-
final @NotNull Member main = targetMember.get().altOwner().orElse(targetMember.get());
63-
final @NotNull HashSet<@NotNull Member> alts = main.getAlts();
64-
65-
SMPCore.runMain(() -> main.player().ban(reason, banExpiry, banSource.asString()));
66-
if (alts.isEmpty()) return sendMessage(sender, SMPCore.messages().bannedMember(main, duration));
67-
else {
68-
SMPCore.runMain(() -> {
69-
for (final @NotNull Member alt : alts)
70-
alt.player().ban(reason, banExpiry, banSource.asString());
71-
});
72-
return sendMessage(sender, SMPCore.messages().bannedMemberChain(main, alts.stream().toList(), duration));
73-
}
116+
if (banned.size() == 1)
117+
return sendMessage(sender, SMPCore.messages().bannedMember(banned.get(0), duration));
118+
return sendMessage(sender, SMPCore.messages().bannedMemberChain(
119+
banned.get(0),
120+
banned.subList(1, banned.size()), duration)
121+
);
74122
}
75123

76124
@Override
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package pro.cloudnode.smp.smpcore.listener;
2+
3+
import net.kyori.adventure.text.Component;
4+
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
5+
import org.bukkit.Statistic;
6+
import org.bukkit.entity.Player;
7+
import org.bukkit.event.EventHandler;
8+
import org.bukkit.event.EventPriority;
9+
import org.bukkit.event.Listener;
10+
import org.bukkit.event.entity.PlayerDeathEvent;
11+
import org.jetbrains.annotations.NotNull;
12+
import pro.cloudnode.smp.smpcore.Permission;
13+
import pro.cloudnode.smp.smpcore.SMPCore;
14+
import pro.cloudnode.smp.smpcore.command.BanCommand;
15+
16+
import java.util.Optional;
17+
18+
public final class PlayerDeathListener implements Listener {
19+
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
20+
public void banPlayer(final @NotNull PlayerDeathEvent event) {
21+
if (!SMPCore.config().deathBanEnabled())
22+
return;
23+
24+
final @NotNull Player player = event.getPlayer();
25+
26+
if (player.hasPermission(Permission.DEATHBAN_BYPASS))
27+
return;
28+
29+
player.spigot().respawn();
30+
player.setGameMode(player.getServer().getDefaultGameMode());
31+
32+
final Component reason = Optional.ofNullable(event.deathMessage())
33+
.orElse(SMPCore.config().deathBanMessage());
34+
35+
BanCommand.ban(
36+
player,
37+
PlainTextComponentSerializer.plainText().serialize(reason),
38+
SMPCore.config().deathBanProgression(player.getStatistic(Statistic.DEATHS)),
39+
null
40+
);
41+
}
42+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package pro.cloudnode.smp.smpcore.listener;
2+
3+
import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent;
4+
import org.bukkit.entity.Player;
5+
import org.bukkit.event.EventHandler;
6+
import org.bukkit.event.EventPriority;
7+
import org.bukkit.event.Listener;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
public final class PlayerPostRespawnListener implements Listener {
11+
@EventHandler(priority = EventPriority.HIGHEST)
12+
public void enforceDefaultGamemode(final @NotNull PlayerPostRespawnEvent event) {
13+
final Player player = event.getPlayer();
14+
15+
if (player.hasPermission("minecraft.command.gamemode"))
16+
return;
17+
18+
player.setGameMode(player.getServer().getDefaultGameMode());
19+
}
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package pro.cloudnode.smp.smpcore.listener;
2+
3+
import com.destroystokyo.paper.profile.PlayerProfile;
4+
import io.papermc.paper.ban.BanListType;
5+
import org.bukkit.BanEntry;
6+
import org.bukkit.event.EventHandler;
7+
import org.bukkit.event.EventPriority;
8+
import org.bukkit.event.Listener;
9+
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
import pro.cloudnode.smp.smpcore.SMPCore;
13+
14+
public final class PlayerPreLoginListener implements Listener {
15+
@EventHandler(priority = EventPriority.HIGHEST)
16+
public void formatBanScreen(final @NotNull AsyncPlayerPreLoginEvent event) {
17+
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED)
18+
return;
19+
20+
@Nullable BanEntry<PlayerProfile> banEntry = SMPCore.getInstance().getServer()
21+
.getBanList(BanListType.PROFILE).getBanEntry(event.getPlayerProfile());
22+
23+
if (banEntry == null)
24+
return;
25+
26+
SMPCore.messages().banScreen(banEntry)
27+
.ifPresent(reason -> event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, reason));
28+
}
29+
}

src/main/resources/config.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ join:
1414
# The number of minutes after which requests/invitations to join a nation expire
1515
request-expire-minutes: 1440
1616

17+
# Ban players upon death
18+
death-ban:
19+
enabled: true
20+
message: <lang:deathScreen.title>
21+
22+
# Ban durations
23+
progression:
24+
- PT1M
25+
- PT2M
26+
- PT5M
27+
- PT10M
28+
- PT20M
29+
- PT45M
30+
- PT1H30M
31+
- PT3H
32+
- PT6H
33+
- PT12H
34+
- P1D
35+
- P3D
36+
- P7D
37+
- P14D
38+
1739
relative-time:
1840
seconds: <t> <format:'0#seconds|1#second|1<seconds'>
1941
minutes: <t> <format:'0#minutes|1#minute|1<minutes'>

src/main/resources/messages.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ nation:
8383
request-rejected: <green>(!) You have rejected the request of <gray><player></gray> to join <gray><nation></gray>.</green>
8484
invite-cancelled: <green>(!) You have cancelled the invitation to <gray><player></gray> to join <gray><nation></gray>.</green>
8585
invite-rejected: <green>(!) You have rejected the invitation to join <gray><nation></gray>.</green>
86+
87+
ban-screen:
88+
permanent: |-
89+
<red><lang:multiplayer.disconnect.banned></red>
90+
91+
<reason>
92+
93+
temporary: |-
94+
<red><lang:multiplayer.disconnect.banned></red>
95+
96+
<reason>
97+
<gray><lang:multiplayer.disconnect.banned.expiration:'<white><expiration:"yyyy-MM-dd"></white> at <white><expiration:"HH:mm:ss z"></white>'> (<expiration-relative>).</gray>
98+
8699
error:
87100
no-permission: <red>(!) You don't have permission to use this command.</red>
88101
player-not-banned: <red>(!) Player <gray><player></gray> is not banned and is not a member.</red>

0 commit comments

Comments
 (0)