diff --git a/src/main/java/net/kewwbec/social/SocialPlugin.java b/src/main/java/net/kewwbec/social/SocialPlugin.java index 56bf37d..3da9ee0 100644 --- a/src/main/java/net/kewwbec/social/SocialPlugin.java +++ b/src/main/java/net/kewwbec/social/SocialPlugin.java @@ -124,7 +124,7 @@ public final class SocialPlugin extends JavaPlugin { getCommandRegistry().registerCommand(new FriendCommand(friends, resolver)); getCommandRegistry().registerCommand(new GuildCommand(guilds, resolver, chatTargets)); - getCommandRegistry().registerCommand(new MsgCommand(chatTargets, resolver)); + getCommandRegistry().registerCommand(new MsgCommand(chatTargets, resolver, dms)); getCommandRegistry().registerCommand(new ReplyCommand(chatTargets, dms, presence)); getCommandRegistry().registerCommand(new ChatCommand(chatTargets)); getCommandRegistry().registerCommand(new PrivacyCommand(privacy)); diff --git a/src/main/java/net/kewwbec/social/command/dm/MsgCommand.java b/src/main/java/net/kewwbec/social/command/dm/MsgCommand.java index 427fdaf..438ea6e 100644 --- a/src/main/java/net/kewwbec/social/command/dm/MsgCommand.java +++ b/src/main/java/net/kewwbec/social/command/dm/MsgCommand.java @@ -4,6 +4,7 @@ import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; @@ -13,27 +14,34 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.social.SocialPermissionsNodes; import net.kewwbec.social.service.ChatTargetService; +import net.kewwbec.social.service.DmRoutingService; import net.kewwbec.social.util.PlayerResolver; import javax.annotation.Nonnull; import java.awt.Color; /** - * /msg enters one-shot DM mode for the caller. The very next chat message they send is - * routed to the named player via SocialChatRouter, then their chat falls back to NORMAL. + * /msg <name> [message...] * - * Hytale args are single-token so we can't take the body as part of the command. + * If a message body is supplied, sends it once and leaves chat mode alone. + * If only a name is supplied, enters one-shot DM mode for the caller - the next chat message they + * send is routed as a DM to that target, then the mode reverts to NORMAL. */ public final class MsgCommand extends AbstractPlayerCommand { private final ChatTargetService targets; private final PlayerResolver resolver; + private final DmRoutingService dms; private final RequiredArg nameArg = withRequiredArg("name", "Recipient", ArgTypes.STRING); + private final OptionalArg bodyArg = withOptionalArg("message", "Message body (optional)", ArgTypes.GREEDY_STRING); - public MsgCommand(@Nonnull ChatTargetService targets, @Nonnull PlayerResolver resolver) { - super("msg", "Start a DM (your next chat message goes to that player)"); + public MsgCommand(@Nonnull ChatTargetService targets, + @Nonnull PlayerResolver resolver, + @Nonnull DmRoutingService dms) { + super("msg", "Send a one-shot DM (or enter DM mode if no message given)"); this.targets = targets; this.resolver = resolver; + this.dms = dms; requirePermission(SocialPermissionsNodes.DM_USE); } @@ -43,7 +51,7 @@ public final class MsgCommand extends AbstractPlayerCommand { protected void execute(@Nonnull CommandContext ctx, @Nonnull Store store, @Nonnull Ref ref, @Nonnull PlayerRef sender, @Nonnull World world) { String name = nameArg.get(ctx); if (name == null) { - ctx.sendMessage(Message.raw("Usage: /msg ").color(Color.YELLOW)); + ctx.sendMessage(Message.raw("Usage: /msg [message...]").color(Color.YELLOW)); return; } PlayerLocation target = resolver.resolve(name); @@ -51,7 +59,28 @@ public final class MsgCommand extends AbstractPlayerCommand { ctx.sendMessage(Message.raw(name + " is not online.").color(Color.RED)); return; } + + String body = bodyArg.get(ctx); + if (body != null && !body.isBlank()) { + DmRoutingService.Result r = dms.send(sender, target.username(), body.trim()); + if (r != DmRoutingService.Result.OK) { + ctx.sendMessage(Message.raw(errorText(r, target.username())).color(Color.RED)); + } + return; + } + targets.setDm(sender.getUuid(), target.uuid(), target.username()); ctx.sendMessage(Message.raw("DM mode -> " + target.username() + ". Type your next chat to send.").color(Color.LIGHT_GRAY)); } + + @Nonnull + private static String errorText(@Nonnull DmRoutingService.Result r, @Nonnull String targetName) { + return switch (r) { + case SELF -> "You can't DM yourself."; + case TARGET_OFFLINE -> targetName + " is not online."; + case BLOCKED_BY_PRIVACY -> "You can't DM " + targetName + " right now."; + case DB_ERROR -> "DB error sending DM."; + case OK -> ""; + }; + } } diff --git a/src/main/java/net/kewwbec/social/command/dm/ReplyCommand.java b/src/main/java/net/kewwbec/social/command/dm/ReplyCommand.java index 3246f72..177d323 100644 --- a/src/main/java/net/kewwbec/social/command/dm/ReplyCommand.java +++ b/src/main/java/net/kewwbec/social/command/dm/ReplyCommand.java @@ -4,6 +4,8 @@ import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; @@ -19,18 +21,23 @@ import java.awt.Color; import java.util.UUID; /** - * /r enters DM mode targeting whoever DMed you last. Then your next chat goes to them. + * /r [message...] + * + * If a body is supplied, sends it once to whoever DMed you last. + * If no body, enters one-shot DM mode targeting that player - the next chat message you send goes + * to them, then mode reverts to NORMAL. */ public final class ReplyCommand extends AbstractPlayerCommand { private final ChatTargetService targets; private final DmRoutingService dms; private final PlayerPresence presence; + private final OptionalArg bodyArg = withOptionalArg("message", "Message body (optional)", ArgTypes.GREEDY_STRING); public ReplyCommand(@Nonnull ChatTargetService targets, @Nonnull DmRoutingService dms, @Nonnull PlayerPresence presence) { - super("r", "Reply to your last DM sender"); + super("r", "Reply to your last DM sender (one-shot send, or enter DM mode if no body given)"); this.targets = targets; this.dms = dms; this.presence = presence; @@ -51,7 +58,28 @@ public final class ReplyCommand extends AbstractPlayerCommand { ctx.sendMessage(Message.raw("That person is offline.").color(Color.RED)); return; } + + String body = bodyArg.get(ctx); + if (body != null && !body.isBlank()) { + DmRoutingService.Result r = dms.send(sender, target.username(), body.trim()); + if (r != DmRoutingService.Result.OK) { + ctx.sendMessage(Message.raw(errorText(r, target.username())).color(Color.RED)); + } + return; + } + targets.setDm(sender.getUuid(), target.uuid(), target.username()); ctx.sendMessage(Message.raw("Reply -> " + target.username() + ". Type your next chat to send.").color(Color.LIGHT_GRAY)); } + + @Nonnull + private static String errorText(@Nonnull DmRoutingService.Result r, @Nonnull String targetName) { + return switch (r) { + case SELF -> "You can't DM yourself."; + case TARGET_OFFLINE -> targetName + " is not online."; + case BLOCKED_BY_PRIVACY -> "You can't DM " + targetName + " right now."; + case DB_ERROR -> "DB error sending DM."; + case OK -> ""; + }; + } } diff --git a/src/main/java/net/kewwbec/social/ui/FriendsListPage.java b/src/main/java/net/kewwbec/social/ui/FriendsListPage.java index f8235a2..6a492a1 100644 --- a/src/main/java/net/kewwbec/social/ui/FriendsListPage.java +++ b/src/main/java/net/kewwbec/social/ui/FriendsListPage.java @@ -12,7 +12,6 @@ import net.kewwbec.networkcore.NetworkCore; import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.social.model.Friendship; -import net.kewwbec.social.service.FriendService; import javax.annotation.Nonnull; import java.sql.SQLException; @@ -22,88 +21,82 @@ import java.util.UUID; import java.util.function.Consumer; import java.util.logging.Level; +/** + * Three tabs (Friends / Incoming / Outgoing) rendered with HyUI's native tab system, so switching + * tabs is purely client-side - no page rebuild, no flicker. Row action buttons (remove, accept, + * reject, cancel) trigger a rebuild after the action so the lists refresh. + */ public final class FriendsListPage { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); - public enum Tab { FRIENDS, INCOMING, OUTGOING } - private final PlayerRef playerRef; private final SocialUIContext ctx; - private final Tab tab; private final PageBuilder page; public FriendsListPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) { - this(playerRef, ctx, Tab.FRIENDS); - } - - public FriendsListPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx, @Nonnull Tab tab) { this.playerRef = playerRef; this.ctx = ctx; - this.tab = tab; - - StringBuilder body = new StringBuilder(); - body.append("

Tabs:

"); - body.append(""); - body.append(""); - body.append(""); - body.append("

 

"); List actions = new ArrayList<>(); + String friendsBody; + String incomingBody; + String outgoingBody; try { - switch (tab) { - case FRIENDS -> renderFriends(body, actions); - case INCOMING -> renderIncoming(body, actions); - case OUTGOING -> renderOutgoing(body, actions); - } + friendsBody = renderFriends(actions); + incomingBody = renderIncoming(actions); + outgoingBody = renderOutgoing(actions); } catch (SQLException e) { ((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Friends panel render failed"); - body.append("

DB error: ").append(escape(e.getMessage())).append("

"); + String err = "

DB error: " + escape(e.getMessage()) + "

"; + friendsBody = err; + incomingBody = err; + outgoingBody = err; } - body.append("

 

"); - body.append(""); - - String html = """ -
-
-
- %s -
-
+ String body = """ + +
+ %s
- """.formatted(body.toString()); +
+ %s +
+
+ %s +
+

 

+ + """.formatted(friendsBody, incomingBody, outgoingBody); + + String html = PageLayout.wrap("Friends", body); this.page = PageBuilder.pageForPlayer(playerRef) .withLifetime(CustomPageLifetime.CanDismiss) .fromHtml(html); - page.addEventListener("tabFriends", CustomUIEventBindingType.Activating, (ignored, pCtx) -> - openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.FRIENDS).open(s))); - page.addEventListener("tabIncoming", CustomUIEventBindingType.Activating, (ignored, pCtx) -> - openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.INCOMING).open(s))); - page.addEventListener("tabOutgoing", CustomUIEventBindingType.Activating, (ignored, pCtx) -> - openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.OUTGOING).open(s))); page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> openInStore(s -> new SocialHubPage(playerRef, ctx).open(s))); for (RowAction a : actions) { - String btnId = a.buttonId; - Tab cur = this.tab; - page.addEventListener(btnId, CustomUIEventBindingType.Activating, (ignored, pCtx) -> { + page.addEventListener(a.buttonId, CustomUIEventBindingType.Activating, (ignored, pCtx) -> { a.runnable.run(); - openInStore(s -> new FriendsListPage(playerRef, ctx, cur).open(s)); + openInStore(s -> new FriendsListPage(playerRef, ctx).open(s)); }); } } - private void renderFriends(@Nonnull StringBuilder body, @Nonnull List actions) throws SQLException { + @Nonnull + private String renderFriends(@Nonnull List actions) throws SQLException { List friends = ctx.friends.list(playerRef.getUuid()); - body.append("

Friends (").append(friends.size()).append(")

"); + StringBuilder sb = new StringBuilder(); + sb.append("

Friends (").append(friends.size()).append(")

"); if (friends.isEmpty()) { - body.append("

None yet. Use /friend add <name> to send a request.

"); - return; + sb.append("

None yet. Use /friend add <name> to send a request.

"); + return sb.toString(); } PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence(); int i = 0; @@ -112,9 +105,9 @@ public final class FriendsListPage { String otherName = f.fromUuid().equals(playerRef.getUuid()) ? f.toName() : f.fromName(); PlayerLocation loc = (presence == null) ? null : presence.getLocation(otherUuid); String status = (loc == null) ? "offline" : "on " + loc.serverId(); - body.append("

").append(escape(otherName)).append(" (").append(escape(status)).append(")

"); + sb.append("

").append(escape(otherName)).append(" (").append(escape(status)).append(")

"); String removeId = "removeFriend" + i; - body.append(""); + sb.append(""); UUID finalOther = otherUuid; String finalName = otherName; actions.add(new RowAction(removeId, () -> @@ -122,24 +115,27 @@ public final class FriendsListPage { )); i++; } + return sb.toString(); } - private void renderIncoming(@Nonnull StringBuilder body, @Nonnull List actions) throws SQLException { + @Nonnull + private String renderIncoming(@Nonnull List actions) throws SQLException { List incoming = ctx.friends.incomingPending(playerRef.getUuid()); - body.append("

Incoming requests (").append(incoming.size()).append(")

"); + StringBuilder sb = new StringBuilder(); + sb.append("

Incoming requests (").append(incoming.size()).append(")

"); if (incoming.isEmpty()) { - body.append("

No one has sent you a friend request.

"); - return; + sb.append("

No one has sent you a friend request.

"); + return sb.toString(); } int i = 0; for (Friendship f : incoming) { UUID fromUuid = f.fromUuid(); String fromName = f.fromName(); - body.append("

from ").append(escape(fromName)).append("

"); + sb.append("

from ").append(escape(fromName)).append("

"); String acceptId = "accept" + i; String rejectId = "reject" + i; - body.append(""); - body.append(""); + sb.append(""); + sb.append(""); actions.add(new RowAction(acceptId, () -> ctx.friends.accept(fromUuid, fromName, playerRef.getUuid(), playerRef.getUsername()) )); @@ -148,27 +144,31 @@ public final class FriendsListPage { )); i++; } + return sb.toString(); } - private void renderOutgoing(@Nonnull StringBuilder body, @Nonnull List actions) throws SQLException { + @Nonnull + private String renderOutgoing(@Nonnull List actions) throws SQLException { List outgoing = ctx.friends.outgoingPending(playerRef.getUuid()); - body.append("

Outgoing requests (").append(outgoing.size()).append(")

"); + StringBuilder sb = new StringBuilder(); + sb.append("

Outgoing requests (").append(outgoing.size()).append(")

"); if (outgoing.isEmpty()) { - body.append("

No outgoing requests.

"); - return; + sb.append("

No outgoing requests.

"); + return sb.toString(); } int i = 0; for (Friendship f : outgoing) { UUID toUuid = f.toUuid(); String toName = f.toName(); - body.append("

to ").append(escape(toName)).append("

"); + sb.append("

to ").append(escape(toName)).append("

"); String cancelId = "cancel" + i; - body.append(""); + sb.append(""); actions.add(new RowAction(cancelId, () -> ctx.friends.remove(playerRef.getUuid(), playerRef.getUsername(), toUuid, toName) )); i++; } + return sb.toString(); } public void open(@Nonnull Store store) { @@ -181,11 +181,6 @@ public final class FriendsListPage { action.accept(ref.getStore()); } - @Nonnull - private static String marker(@Nonnull Tab current, @Nonnull Tab target) { - return current == target ? "[*] " : "[ ] "; - } - @Nonnull private static String escape(@Nonnull String s) { return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); diff --git a/src/main/java/net/kewwbec/social/ui/GuildRankEditorPage.java b/src/main/java/net/kewwbec/social/ui/GuildRankEditorPage.java new file mode 100644 index 0000000..e595a13 --- /dev/null +++ b/src/main/java/net/kewwbec/social/ui/GuildRankEditorPage.java @@ -0,0 +1,144 @@ +package net.kewwbec.social.ui; + +import au.ellie.hyui.builders.PageBuilder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import net.kewwbec.social.model.GuildPermission; +import net.kewwbec.social.model.GuildRank; +import net.kewwbec.social.service.GuildService; + +import javax.annotation.Nonnull; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +/** + * Lists ranks for the caller's guild; each non-owner rank can be edited or deleted. Plus a button + * to open the form for a new rank. The owner rank is shown but locked. + * + * Requires MANAGE_RANKS on the caller's rank to actually mutate; the service enforces this. + */ +public final class GuildRankEditorPage { + + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + private final PlayerRef playerRef; + private final SocialUIContext ctx; + private final PageBuilder page; + + public GuildRankEditorPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) { + this.playerRef = playerRef; + this.ctx = ctx; + + GuildService.GuildContext gc = ctx.guilds.contextFor(playerRef.getUuid()); + StringBuilder body = new StringBuilder(); + List actions = new ArrayList<>(); + + if (gc == null) { + body.append("

You are not in a guild.

"); + } else { + boolean canManage = GuildPermission.MANAGE_RANKS.isIn(gc.rank().permissions()); + body.append("

Guild: ").append(escape(gc.guild().name())).append("

"); + body.append("

Your rank: ").append(escape(gc.rank().name())).append("

"); + if (!canManage) { + body.append("

You can't edit ranks. Need MANAGE_RANKS permission on your rank.

"); + } + body.append("

 

"); + + try { + List ranks = ctx.guilds.rankList(gc.guild().id()); + body.append("

Ranks (highest first)

"); + int i = 0; + for (GuildRank r : ranks) { + body.append("

").append(escape(r.name())).append("") + .append(" - id=").append(r.rankId()) + .append(" prio=").append(r.priority()) + .append(r.isOwnerRank() ? " [OWNER, locked]" : "") + .append("

"); + body.append("

").append(escape(describePerms(r.permissions()))).append("

"); + + if (canManage && !r.isOwnerRank()) { + String editId = "edit" + i; + String delId = "del" + i; + body.append(""); + body.append(""); + int rankId = r.rankId(); + actions.add(new Action(editId, () -> + openInStore(s -> new GuildRankFormPage(playerRef, ctx, rankId).open(s)) + )); + actions.add(new Action(delId, () -> { + ctx.guilds.deleteRank(playerRef.getUuid(), rankId); + openInStore(s -> new GuildRankEditorPage(playerRef, ctx).open(s)); + })); + } + body.append("

 

"); + i++; + } + } catch (SQLException e) { + ((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Rank list failed"); + body.append("

DB error: ").append(escape(e.getMessage())).append("

"); + } + + if (canManage) { + body.append(""); + actions.add(new Action("newBtn", () -> + openInStore(s -> new GuildRankFormPage(playerRef, ctx, -1).open(s)) + )); + } + } + + body.append("

 

"); + body.append(""); + + String html = PageLayout.wrap("Guild Ranks", body.toString()); + + this.page = PageBuilder.pageForPlayer(playerRef) + .withLifetime(CustomPageLifetime.CanDismiss) + .fromHtml(html); + + for (Action a : actions) { + page.addEventListener(a.id, CustomUIEventBindingType.Activating, (ignored, pCtx) -> a.runnable.run()); + } + page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> + openInStore(s -> new GuildRosterPage(playerRef, ctx).open(s))); + } + + public void open(@Nonnull Store store) { + page.open(store); + } + + private void openInStore(@Nonnull Consumer> action) { + Ref ref = playerRef.getReference(); + if (ref == null) return; + action.accept(ref.getStore()); + } + + @Nonnull + private static String describePerms(int mask) { + if (mask == 0) return "(no permissions)"; + StringBuilder sb = new StringBuilder(); + for (GuildPermission p : GuildPermission.values()) { + if (p.isIn(mask)) { + if (sb.length() > 0) sb.append(", "); + sb.append(p.name()); + } + } + return sb.toString(); + } + + @Nonnull + private static String escape(@Nonnull String s) { + return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } + + private record Action(@Nonnull String id, @Nonnull Runnable runnable) {} +} diff --git a/src/main/java/net/kewwbec/social/ui/GuildRankFormPage.java b/src/main/java/net/kewwbec/social/ui/GuildRankFormPage.java new file mode 100644 index 0000000..491d6ef --- /dev/null +++ b/src/main/java/net/kewwbec/social/ui/GuildRankFormPage.java @@ -0,0 +1,161 @@ +package net.kewwbec.social.ui; + +import au.ellie.hyui.builders.PageBuilder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import net.kewwbec.social.model.GuildPermission; +import net.kewwbec.social.model.GuildRank; +import net.kewwbec.social.service.GuildService; + +import javax.annotation.Nonnull; +import java.sql.SQLException; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.logging.Level; + +/** + * Form to edit one rank (or create a new one if rankId is -1). Shows a name field, priority field, + * and a checkbox per GuildPermission. Save calls the appropriate service method; Cancel returns + * to the editor list. + */ +public final class GuildRankFormPage { + + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + private final PlayerRef playerRef; + private final SocialUIContext ctx; + private final int rankId; + private final PageBuilder page; + + public GuildRankFormPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx, int rankId) { + this.playerRef = playerRef; + this.ctx = ctx; + this.rankId = rankId; + + GuildService.GuildContext gc = ctx.guilds.contextFor(playerRef.getUuid()); + StringBuilder body = new StringBuilder(); + String title = "Edit rank"; + boolean isNew = (rankId < 0); + boolean formReady = false; + String nameValue = ""; + int priorityValue = 10; + int permsValue = GuildPermission.CHAT.bit(); + + if (gc == null) { + body.append("

You are not in a guild.

"); + } else if (isNew) { + title = "New rank"; + formReady = true; + } else { + try { + Optional existing = findRank(gc.guild().id(), rankId); + if (existing.isPresent()) { + nameValue = existing.get().name(); + priorityValue = existing.get().priority(); + permsValue = existing.get().permissions(); + formReady = true; + } else { + body.append("

Rank not found.

"); + } + } catch (SQLException e) { + ((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Rank lookup failed"); + body.append("

DB error: ").append(escape(e.getMessage())).append("

"); + } + } + + if (formReady) { + body.append("

").append(isNew ? "New rank" : "Edit rank").append("

"); + body.append("

Name:

"); + body.append(""); + body.append("

Priority (higher = outranks lower):

"); + body.append(""); + body.append("

 

"); + body.append("

Permissions

"); + + for (GuildPermission p : GuildPermission.values()) { + if (p == GuildPermission.DISBAND) continue; + boolean checked = p.isIn(permsValue); + body.append(""); + body.append(""); + body.append("

 

"); + } + + body.append(""); + } + body.append(""); + + String html = PageLayout.wrap(title, body.toString()); + this.page = PageBuilder.pageForPlayer(playerRef) + .withLifetime(CustomPageLifetime.CanDismiss) + .fromHtml(html); + + page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> + openInStore(s -> new GuildRankEditorPage(playerRef, ctx).open(s))); + + if (formReady) { + page.addEventListener("saveBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> { + String name = pCtx.getValue("nameInput", String.class).orElse("").trim(); + Integer priority = pCtx.getValue("priorityInput", Integer.class).orElse(10); + int mask = 0; + for (GuildPermission p : GuildPermission.values()) { + if (p == GuildPermission.DISBAND) continue; + boolean checked = pCtx.getValue("perm" + p.name(), Boolean.class).orElse(false); + if (checked) mask |= p.bit(); + } + if (name.isEmpty()) { + return; + } + if (rankId < 0) { + ctx.guilds.createRank(playerRef.getUuid(), name, priority, mask); + } else { + ctx.guilds.updateRank(playerRef.getUuid(), rankId, name, priority, mask); + } + openInStore(s -> new GuildRankEditorPage(playerRef, ctx).open(s)); + }); + } + } + + @Nonnull + private Optional findRank(long guildId, int targetRankId) throws SQLException { + for (GuildRank r : ctx.guilds.rankList(guildId)) { + if (r.rankId() == targetRankId) return Optional.of(r); + } + return Optional.empty(); + } + + public void open(@Nonnull Store store) { + page.open(store); + } + + private void openInStore(@Nonnull Consumer> action) { + Ref ref = playerRef.getReference(); + if (ref == null) return; + action.accept(ref.getStore()); + } + + @Nonnull + private static String describe(@Nonnull GuildPermission p) { + return switch (p) { + case INVITE -> "invite players to the guild"; + case KICK -> "kick lower-ranked members"; + case PROMOTE -> "promote members to a higher rank"; + case DEMOTE -> "demote members to a lower rank"; + case MOTD -> "change the guild MOTD"; + case MANAGE_RANKS -> "create / edit / delete ranks"; + case DISBAND -> "(owner only)"; + case CHAT -> "speak in guild chat"; + }; + } + + @Nonnull + private static String escape(@Nonnull String s) { + return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } +} diff --git a/src/main/java/net/kewwbec/social/ui/GuildRosterPage.java b/src/main/java/net/kewwbec/social/ui/GuildRosterPage.java index 6efeefd..097e08d 100644 --- a/src/main/java/net/kewwbec/social/ui/GuildRosterPage.java +++ b/src/main/java/net/kewwbec/social/ui/GuildRosterPage.java @@ -102,17 +102,11 @@ public final class GuildRosterPage { } body.append("

 

"); + if (gc != null) { + body.append(""); + } body.append(""); - - String html = """ -
-
-
- %s -
-
-
- """.formatted(escape(title), body.toString()); + String html = PageLayout.wrap(title, body.toString()); this.page = PageBuilder.pageForPlayer(playerRef) .withLifetime(CustomPageLifetime.CanDismiss) @@ -120,6 +114,11 @@ public final class GuildRosterPage { page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> openInStore(s -> new SocialHubPage(playerRef, ctx).open(s))); + + if (gc != null) { + page.addEventListener("ranksBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> + openInStore(s -> new GuildRankEditorPage(playerRef, ctx).open(s))); + } } public void open(@Nonnull Store store) { diff --git a/src/main/java/net/kewwbec/social/ui/PrivacySettingsPage.java b/src/main/java/net/kewwbec/social/ui/PrivacySettingsPage.java index 39b4d76..771b060 100644 --- a/src/main/java/net/kewwbec/social/ui/PrivacySettingsPage.java +++ b/src/main/java/net/kewwbec/social/ui/PrivacySettingsPage.java @@ -55,16 +55,7 @@ public final class PrivacySettingsPage { body.append("

 

"); body.append(""); - - String html = """ -
-
-
- %s -
-
-
- """.formatted(body.toString()); + String html = PageLayout.wrap("Privacy", body.toString()); this.page = PageBuilder.pageForPlayer(playerRef) .withLifetime(CustomPageLifetime.CanDismiss) diff --git a/src/main/java/net/kewwbec/social/ui/SocialHubPage.java b/src/main/java/net/kewwbec/social/ui/SocialHubPage.java index 467cf3a..5137b5d 100644 --- a/src/main/java/net/kewwbec/social/ui/SocialHubPage.java +++ b/src/main/java/net/kewwbec/social/ui/SocialHubPage.java @@ -27,24 +27,21 @@ public final class SocialHubPage { int incoming = countIncomingRequests(); String guildLine = guildSummary(); - String html = """ -
-
-
-

Signed in as %s.

-

 

-

Friends

- -

Guild

- -

Privacy

- -

 

- -
-
-
- """.formatted(escape(playerRef.getUsername()), friendCount, incoming, escape(guildLine)); + String body = """ +

Signed in as %s.

+

 

+

Friends

+ +

 

+

Guild

+ +

 

+

Privacy

+ +

 

+ + """.formatted(PageLayout.escape(playerRef.getUsername()), friendCount, incoming, PageLayout.escape(guildLine)); + String html = PageLayout.wrap("Social", body); this.page = PageBuilder.pageForPlayer(playerRef) .withLifetime(CustomPageLifetime.CanDismiss)