Skip to content

Commit f11860e

Browse files
committed
add guest system
1 parent 58c2f1a commit f11860e

19 files changed

Lines changed: 630 additions & 11 deletions

‎src/main/java/dev/plex/Guilds.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public void enable()
103103
registerListener(guildWorldProtectionListener);
104104
registerListener(new GuildWorldEntityProtectionListener(this));
105105
registerListener(guildWorldAccessListener);
106+
guildWorldAccessListener.startGuestExpiry();
106107
registerListener(rankPermissionMenuListener);
107108
}
108109

‎src/main/java/dev/plex/command/GuildCommand.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public GuildCommand(Guilds module)
3333
this.registerSubCommand(new InviteSubCommand(module));
3434
this.registerSubCommand(new AcceptSubCommand(module));
3535
this.registerSubCommand(new WorldSubCommand(module));
36+
this.registerSubCommand(new VisitSubCommand(module));
37+
this.registerSubCommand(new GuestAccessSubCommand(module, true));
38+
this.registerSubCommand(new GuestAccessSubCommand(module, false));
39+
this.registerSubCommand(new GuestsSubCommand(module));
3640
this.registerSubCommand(new HomeSubCommand(module));
3741
this.registerSubCommand(new ChatSubCommand(module));
3842
this.registerSubCommand(new WarpSubCommand(module));
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package dev.plex.command.sub;
2+
3+
import dev.plex.Guilds;
4+
import dev.plex.command.source.RequiredCommandSource;
5+
import dev.plex.guild.Guild;
6+
import dev.plex.guild.data.Guest;
7+
import dev.plex.guild.data.GuildPermission;
8+
import java.time.Instant;
9+
import java.time.temporal.ChronoUnit;
10+
import java.util.List;
11+
import java.util.UUID;
12+
import java.util.concurrent.CompletableFuture;
13+
import java.util.concurrent.CompletionException;
14+
import net.kyori.adventure.text.Component;
15+
import net.kyori.adventure.text.event.ClickEvent;
16+
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
17+
import org.bukkit.Bukkit;
18+
import org.bukkit.command.CommandSender;
19+
import org.bukkit.entity.Player;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
public final class GuestAccessSubCommand extends GuildSubCommand
24+
{
25+
private final boolean grant;
26+
27+
public GuestAccessSubCommand(Guilds module, boolean grant)
28+
{
29+
super(module, command(grant ? "guest" : "unguest")
30+
.description(grant ? "Give temporary world access" : "Remove guest access")
31+
.usage(grant ? "/guild <command> <player name or UUID> <view|edit> <minutes>"
32+
: "/guild <command> <player name or UUID>")
33+
.permission("plex.guilds.guests")
34+
.source(RequiredCommandSource.IN_GAME)
35+
.build());
36+
this.grant = grant;
37+
}
38+
39+
@Override
40+
public Component executeSubCommand(@NotNull CommandSender sender, @Nullable Player player,
41+
@Nullable String first, @Nullable String remaining)
42+
{
43+
assert player != null;
44+
if (first == null || (!grant && remaining != null))
45+
{
46+
return usage();
47+
}
48+
Guild guild = module.getGuildHolder().guild(player.getUniqueId()).orElse(null);
49+
if (guild == null)
50+
{
51+
return messageComponent("guildNotFound");
52+
}
53+
if (!guild.hasPermission(player.getUniqueId(), GuildPermission.MANAGE_GUESTS))
54+
{
55+
return messageComponent("guildGuestManagementDenied");
56+
}
57+
boolean editing = false;
58+
Instant expiresAt = null;
59+
if (grant)
60+
{
61+
String[] options = remaining == null ? new String[0] : remaining.split(" ");
62+
if (options.length != 2 || !(options[0].equalsIgnoreCase("view") || options[0].equalsIgnoreCase("edit")))
63+
{
64+
return usage();
65+
}
66+
try
67+
{
68+
int minutes = Integer.parseInt(options[1]);
69+
if (minutes <= 0)
70+
{
71+
return messageComponent("guildGuestDurationInvalid");
72+
}
73+
expiresAt = Instant.now().plus(minutes, ChronoUnit.MINUTES);
74+
}
75+
catch (NumberFormatException exception)
76+
{
77+
return messageComponent("guildGuestDurationInvalid");
78+
}
79+
editing = options[0].equalsIgnoreCase("edit");
80+
}
81+
changeAccess(player, guild, first, editing, expiresAt);
82+
return null;
83+
}
84+
85+
private CompletableFuture<UUID> resolveTarget(String target)
86+
{
87+
try
88+
{
89+
return CompletableFuture.completedFuture(UUID.fromString(target));
90+
}
91+
catch (IllegalArgumentException exception)
92+
{
93+
return module.api().players().byName(target).thenApply(result -> result.map(view -> view.uuid()).orElse(null));
94+
}
95+
}
96+
97+
private void changeAccess(Player player, Guild guild, String target, boolean editing, Instant expiresAt)
98+
{
99+
UUID managerId = player.getUniqueId();
100+
resolveTarget(target).thenCompose(targetId ->
101+
{
102+
if (targetId == null)
103+
{
104+
player.sendMessage(messageComponent("playerNotFound"));
105+
return CompletableFuture.completedFuture(null);
106+
}
107+
CompletableFuture<Void> mutation = grant
108+
? module.getGuildMutationService().grantGuest(guild, managerId, new Guest(targetId, editing, expiresAt))
109+
: module.getGuildMutationService().revokeGuest(guild, managerId, targetId);
110+
return mutation.thenRun(() ->
111+
{
112+
player.sendMessage(messageComponent(grant ? "guildGuestGranted" : "guildGuestRevoked",
113+
Placeholder.unparsed("player", target),
114+
Placeholder.unparsed("mode", editing ? "editing" : "view-only"),
115+
Placeholder.unparsed("expires", String.valueOf(expiresAt))));
116+
if (grant)
117+
{
118+
notifyGuest(guild, new Guest(targetId, editing, expiresAt));
119+
}
120+
});
121+
}).exceptionally(failure ->
122+
{
123+
Throwable cause = failure;
124+
while (cause instanceof CompletionException && cause.getCause() != null)
125+
{
126+
cause = cause.getCause();
127+
}
128+
if (cause instanceof SecurityException)
129+
{
130+
player.sendMessage(messageComponent("guildGuestManagementDenied"));
131+
}
132+
else if (cause instanceof IllegalArgumentException)
133+
{
134+
player.sendMessage(messageComponent("guildGuestTargetInvalid"));
135+
}
136+
else
137+
{
138+
module.getLogger().error("Failed to change guest access for {} in guild {}", target, guild.getGuildUuid(), failure);
139+
player.sendMessage(messageComponent("guildStorageFailed"));
140+
}
141+
return null;
142+
});
143+
}
144+
145+
private void notifyGuest(Guild guild, Guest guest)
146+
{
147+
module.ownTask(Bukkit.getGlobalRegionScheduler().run(module.plugin(), task ->
148+
{
149+
if (!module.isReady() || module.getGuildHolder().guildById(guild.getGuildUuid()).orElse(null) != guild
150+
|| !guest.equals(guild.getActiveGuest(guest.playerUuid())))
151+
{
152+
return;
153+
}
154+
Player target = Bukkit.getPlayer(guest.playerUuid());
155+
if (target != null)
156+
{
157+
target.sendMessage(messageComponent("guildGuestReceived",
158+
Placeholder.unparsed("guild", guild.getName()),
159+
Placeholder.unparsed("mode", guest.editing() ? "editing" : "view-only"),
160+
Placeholder.unparsed("expires", guest.expiresAt().toString()))
161+
.clickEvent(ClickEvent.runCommand("/guild visit " + guild.getGuildUuid())));
162+
}
163+
}));
164+
}
165+
166+
@Override
167+
public @NotNull List<String> suggestSubCommand(@NotNull CommandSender sender, @Nullable String first)
168+
{
169+
return first == null ? module.api().players().onlineNames() : grant ? List.of("view", "edit") : List.of();
170+
}
171+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package dev.plex.command.sub;
2+
3+
import dev.plex.Guilds;
4+
import dev.plex.command.source.RequiredCommandSource;
5+
import dev.plex.guild.Guild;
6+
import dev.plex.guild.data.Guest;
7+
import dev.plex.guild.data.GuildPermission;
8+
import java.time.Instant;
9+
import java.util.Comparator;
10+
import java.util.List;
11+
import java.util.concurrent.CompletableFuture;
12+
import net.kyori.adventure.text.Component;
13+
import net.kyori.adventure.text.event.ClickEvent;
14+
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
15+
import org.bukkit.command.CommandSender;
16+
import org.bukkit.entity.Player;
17+
import org.jetbrains.annotations.NotNull;
18+
import org.jetbrains.annotations.Nullable;
19+
20+
public final class GuestsSubCommand extends GuildSubCommand
21+
{
22+
private static final int PAGE_SIZE = 5;
23+
24+
public GuestsSubCommand(Guilds module)
25+
{
26+
super(module, command("guests")
27+
.description("List current guests")
28+
.usage("/guild <command> [page]")
29+
.permission("plex.guilds.guests")
30+
.source(RequiredCommandSource.IN_GAME)
31+
.build());
32+
}
33+
34+
@Override
35+
public Component executeSubCommand(@NotNull CommandSender sender, @Nullable Player player,
36+
@Nullable String first, @Nullable String remaining)
37+
{
38+
assert player != null;
39+
if (remaining != null)
40+
{
41+
return usage();
42+
}
43+
int page;
44+
try
45+
{
46+
page = first == null ? 1 : Integer.parseInt(first);
47+
}
48+
catch (NumberFormatException exception)
49+
{
50+
return usage();
51+
}
52+
Guild guild = module.getGuildHolder().guild(player.getUniqueId()).orElse(null);
53+
if (guild == null)
54+
{
55+
return messageComponent("guildNotFound");
56+
}
57+
if (!guild.hasPermission(player.getUniqueId(), GuildPermission.MANAGE_GUESTS))
58+
{
59+
return messageComponent("guildGuestManagementDenied");
60+
}
61+
Instant now = Instant.now();
62+
List<Guest> guests = guild.getGuests().values().stream().filter(guest -> guest.isActive(now))
63+
.sorted(Comparator.comparing(Guest::expiresAt).thenComparing(Guest::playerUuid)).toList();
64+
int pages = Math.max(1, (guests.size() + PAGE_SIZE - 1) / PAGE_SIZE);
65+
if (page < 1 || page > pages)
66+
{
67+
return messageComponent("guildGuestsPageInvalid", Placeholder.unparsed("pages", Integer.toString(pages)));
68+
}
69+
if (guests.isEmpty())
70+
{
71+
return messageComponent("guildGuestsEmpty");
72+
}
73+
sendPage(player, guild, guests.subList((page - 1) * PAGE_SIZE, Math.min(page * PAGE_SIZE, guests.size())), page, pages);
74+
return null;
75+
}
76+
77+
private void sendPage(Player player, Guild guild, List<Guest> guests, int page, int pages)
78+
{
79+
List<CompletableFuture<Component>> entries = guests.stream().map(guest ->
80+
module.api().players().player(guest.playerUuid()).thenApply(result -> messageComponent("guildGuestEntry",
81+
Placeholder.unparsed("player", result.map(view -> view.name()).orElse(guest.playerUuid().toString())),
82+
Placeholder.unparsed("mode", guest.editing() ? "edit" : "view"),
83+
Placeholder.unparsed("expires", guest.expiresAt().toString()))
84+
.clickEvent(ClickEvent.suggestCommand("/guild unguest " + guest.playerUuid()))
85+
.hoverEvent(Component.text("Click to fill in the removal command")))).toList();
86+
CompletableFuture.allOf(entries.toArray(CompletableFuture[]::new)).whenComplete((unused, failure) ->
87+
{
88+
if (failure != null)
89+
{
90+
module.getLogger().error("Failed to look up guests for guild {}", guild.getGuildUuid(), failure);
91+
player.sendMessage(messageComponent("guildStorageFailed"));
92+
return;
93+
}
94+
if (!module.isReady() || module.getGuildHolder().guildById(guild.getGuildUuid()).orElse(null) != guild
95+
|| !guild.hasPermission(player.getUniqueId(), GuildPermission.MANAGE_GUESTS))
96+
{
97+
player.sendMessage(messageComponent("guildGuestManagementDenied"));
98+
return;
99+
}
100+
Component content = Component.empty();
101+
for (CompletableFuture<Component> entry : entries)
102+
{
103+
content = content.append(Component.newline()).append(entry.join());
104+
}
105+
Component navigation = Component.empty();
106+
if (page > 1)
107+
{
108+
navigation = navigation.append(messageComponent("guildHelpBack")
109+
.clickEvent(ClickEvent.runCommand("/guild guests " + (page - 1))));
110+
}
111+
if (page < pages)
112+
{
113+
navigation = navigation.append(messageComponent("guildHelpNext")
114+
.clickEvent(ClickEvent.runCommand("/guild guests " + (page + 1))));
115+
}
116+
player.sendMessage(messageComponent("guildGuestsPage",
117+
Placeholder.unparsed("page", Integer.toString(page)),
118+
Placeholder.unparsed("pages", Integer.toString(pages)),
119+
Placeholder.component("content", content), Placeholder.component("navigation", navigation)));
120+
});
121+
}
122+
}

0 commit comments

Comments
 (0)