janky ahh ui + support for /msg with text and /r with text

This commit is contained in:
2026-05-27 09:07:10 -04:00
parent 4361d33ec3
commit b506724829
9 changed files with 458 additions and 114 deletions
@@ -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));
@@ -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 <name> 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 &lt;name&gt; [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<String> nameArg = withRequiredArg("name", "Recipient", ArgTypes.STRING);
private final OptionalArg<String> 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<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
String name = nameArg.get(ctx);
if (name == null) {
ctx.sendMessage(Message.raw("Usage: /msg <name>").color(Color.YELLOW));
ctx.sendMessage(Message.raw("Usage: /msg <name> [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 -> "";
};
}
}
@@ -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<String> 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 -> "";
};
}
}
@@ -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("<p>Tabs:</p>");
body.append("<button id=\"tabFriends\">").append(marker(tab, Tab.FRIENDS)).append("Friends</button>");
body.append("<button id=\"tabIncoming\">").append(marker(tab, Tab.INCOMING)).append("Incoming</button>");
body.append("<button id=\"tabOutgoing\">").append(marker(tab, Tab.OUTGOING)).append("Outgoing</button>");
body.append("<p>&nbsp;</p>");
List<RowAction> 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("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
String err = "<p>DB error: " + escape(e.getMessage()) + "</p>";
friendsBody = err;
incomingBody = err;
outgoingBody = err;
}
body.append("<p>&nbsp;</p>");
body.append("<button id=\"backBtn\">Back to Social</button>");
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Friends">
<div class="container-contents">
%s
</div>
</div>
String body = """
<nav id="friendsTabs" class="tabs"
data-tabs="friends:Friends:friendsContent,incoming:Incoming:incomingContent,outgoing:Outgoing:outgoingContent"
data-selected="friends"></nav>
<div id="friendsContent" class="tab-content" data-hyui-tab-id="friends">
%s
</div>
""".formatted(body.toString());
<div id="incomingContent" class="tab-content" data-hyui-tab-id="incoming">
%s
</div>
<div id="outgoingContent" class="tab-content" data-hyui-tab-id="outgoing">
%s
</div>
<p>&nbsp;</p>
<button id="backBtn">Back to Social</button>
""".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<RowAction> actions) throws SQLException {
@Nonnull
private String renderFriends(@Nonnull List<RowAction> actions) throws SQLException {
List<Friendship> friends = ctx.friends.list(playerRef.getUuid());
body.append("<p><b>Friends (").append(friends.size()).append(")</b></p>");
StringBuilder sb = new StringBuilder();
sb.append("<p><b>Friends (").append(friends.size()).append(")</b></p>");
if (friends.isEmpty()) {
body.append("<p>None yet. Use /friend add &lt;name&gt; to send a request.</p>");
return;
sb.append("<p>None yet. Use /friend add &lt;name&gt; to send a request.</p>");
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("<p>").append(escape(otherName)).append(" (").append(escape(status)).append(")</p>");
sb.append("<p>").append(escape(otherName)).append(" (").append(escape(status)).append(")</p>");
String removeId = "removeFriend" + i;
body.append("<button id=\"").append(removeId).append("\">Remove ").append(escape(otherName)).append("</button>");
sb.append("<button id=\"").append(removeId).append("\" class=\"secondary-button\">Remove ").append(escape(otherName)).append("</button>");
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<RowAction> actions) throws SQLException {
@Nonnull
private String renderIncoming(@Nonnull List<RowAction> actions) throws SQLException {
List<Friendship> incoming = ctx.friends.incomingPending(playerRef.getUuid());
body.append("<p><b>Incoming requests (").append(incoming.size()).append(")</b></p>");
StringBuilder sb = new StringBuilder();
sb.append("<p><b>Incoming requests (").append(incoming.size()).append(")</b></p>");
if (incoming.isEmpty()) {
body.append("<p>No one has sent you a friend request.</p>");
return;
sb.append("<p>No one has sent you a friend request.</p>");
return sb.toString();
}
int i = 0;
for (Friendship f : incoming) {
UUID fromUuid = f.fromUuid();
String fromName = f.fromName();
body.append("<p>from ").append(escape(fromName)).append("</p>");
sb.append("<p>from ").append(escape(fromName)).append("</p>");
String acceptId = "accept" + i;
String rejectId = "reject" + i;
body.append("<button id=\"").append(acceptId).append("\">Accept ").append(escape(fromName)).append("</button>");
body.append("<button id=\"").append(rejectId).append("\">Reject ").append(escape(fromName)).append("</button>");
sb.append("<button id=\"").append(acceptId).append("\">Accept ").append(escape(fromName)).append("</button>");
sb.append("<button id=\"").append(rejectId).append("\" class=\"secondary-button\">Reject ").append(escape(fromName)).append("</button>");
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<RowAction> actions) throws SQLException {
@Nonnull
private String renderOutgoing(@Nonnull List<RowAction> actions) throws SQLException {
List<Friendship> outgoing = ctx.friends.outgoingPending(playerRef.getUuid());
body.append("<p><b>Outgoing requests (").append(outgoing.size()).append(")</b></p>");
StringBuilder sb = new StringBuilder();
sb.append("<p><b>Outgoing requests (").append(outgoing.size()).append(")</b></p>");
if (outgoing.isEmpty()) {
body.append("<p>No outgoing requests.</p>");
return;
sb.append("<p>No outgoing requests.</p>");
return sb.toString();
}
int i = 0;
for (Friendship f : outgoing) {
UUID toUuid = f.toUuid();
String toName = f.toName();
body.append("<p>to ").append(escape(toName)).append("</p>");
sb.append("<p>to ").append(escape(toName)).append("</p>");
String cancelId = "cancel" + i;
body.append("<button id=\"").append(cancelId).append("\">Cancel request to ").append(escape(toName)).append("</button>");
sb.append("<button id=\"").append(cancelId).append("\" class=\"secondary-button\">Cancel request to ").append(escape(toName)).append("</button>");
actions.add(new RowAction(cancelId, () ->
ctx.friends.remove(playerRef.getUuid(), playerRef.getUsername(), toUuid, toName)
));
i++;
}
return sb.toString();
}
public void open(@Nonnull Store<EntityStore> 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
@@ -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<Action> actions = new ArrayList<>();
if (gc == null) {
body.append("<p>You are not in a guild.</p>");
} else {
boolean canManage = GuildPermission.MANAGE_RANKS.isIn(gc.rank().permissions());
body.append("<p>Guild: <b>").append(escape(gc.guild().name())).append("</b></p>");
body.append("<p>Your rank: <b>").append(escape(gc.rank().name())).append("</b></p>");
if (!canManage) {
body.append("<p>You can't edit ranks. Need MANAGE_RANKS permission on your rank.</p>");
}
body.append("<p>&nbsp;</p>");
try {
List<GuildRank> ranks = ctx.guilds.rankList(gc.guild().id());
body.append("<p><b>Ranks (highest first)</b></p>");
int i = 0;
for (GuildRank r : ranks) {
body.append("<p><b>").append(escape(r.name())).append("</b>")
.append(" - id=").append(r.rankId())
.append(" prio=").append(r.priority())
.append(r.isOwnerRank() ? " [OWNER, locked]" : "")
.append("</p>");
body.append("<p>").append(escape(describePerms(r.permissions()))).append("</p>");
if (canManage && !r.isOwnerRank()) {
String editId = "edit" + i;
String delId = "del" + i;
body.append("<button id=\"").append(editId).append("\">Edit ")
.append(escape(r.name())).append("</button>");
body.append("<button id=\"").append(delId).append("\" class=\"secondary-button\">Delete ")
.append(escape(r.name())).append("</button>");
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("<p>&nbsp;</p>");
i++;
}
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Rank list failed");
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
}
if (canManage) {
body.append("<button id=\"newBtn\">+ Create new rank</button>");
actions.add(new Action("newBtn", () ->
openInStore(s -> new GuildRankFormPage(playerRef, ctx, -1).open(s))
));
}
}
body.append("<p>&nbsp;</p>");
body.append("<button id=\"backBtn\">Back to Guild</button>");
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<EntityStore> store) {
page.open(store);
}
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
Ref<EntityStore> 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
private record Action(@Nonnull String id, @Nonnull Runnable runnable) {}
}
@@ -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("<p>You are not in a guild.</p>");
} else if (isNew) {
title = "New rank";
formReady = true;
} else {
try {
Optional<GuildRank> 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("<p>Rank not found.</p>");
}
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Rank lookup failed");
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
}
}
if (formReady) {
body.append("<p><b>").append(isNew ? "New rank" : "Edit rank").append("</b></p>");
body.append("<p>Name:</p>");
body.append("<input type=\"text\" id=\"nameInput\" placeholder=\"Rank name\" value=\"")
.append(escape(nameValue)).append("\" />");
body.append("<p>Priority (higher = outranks lower):</p>");
body.append("<input type=\"number\" id=\"priorityInput\" value=\"").append(priorityValue).append("\" />");
body.append("<p>&nbsp;</p>");
body.append("<p><b>Permissions</b></p>");
for (GuildPermission p : GuildPermission.values()) {
if (p == GuildPermission.DISBAND) continue;
boolean checked = p.isIn(permsValue);
body.append("<input type=\"checkbox\" id=\"perm").append(p.name()).append("\"")
.append(checked ? " checked=\"true\"" : "").append(" />");
body.append("<label> ").append(p.name()).append(" - ").append(describe(p)).append("</label>");
body.append("<p>&nbsp;</p>");
}
body.append("<button id=\"saveBtn\">").append(isNew ? "Create" : "Save").append("</button>");
}
body.append("<button id=\"backBtn\" class=\"secondary-button\">").append(formReady ? "Cancel" : "Back").append("</button>");
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<GuildRank> 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<EntityStore> store) {
page.open(store);
}
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
Ref<EntityStore> 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
}
@@ -102,17 +102,11 @@ public final class GuildRosterPage {
}
body.append("<p>&nbsp;</p>");
if (gc != null) {
body.append("<button id=\"ranksBtn\" class=\"secondary-button\">Edit ranks</button>");
}
body.append("<button id=\"backBtn\">Back to Social</button>");
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="%s">
<div class="container-contents">
%s
</div>
</div>
</div>
""".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<EntityStore> store) {
@@ -55,16 +55,7 @@ public final class PrivacySettingsPage {
body.append("<p>&nbsp;</p>");
body.append("<button id=\"backBtn\">Back to Social</button>");
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Privacy">
<div class="container-contents">
%s
</div>
</div>
</div>
""".formatted(body.toString());
String html = PageLayout.wrap("Privacy", body.toString());
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
@@ -27,24 +27,21 @@ public final class SocialHubPage {
int incoming = countIncomingRequests();
String guildLine = guildSummary();
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Social">
<div class="container-contents">
<p>Signed in as %s.</p>
<p>&nbsp;</p>
<p><b>Friends</b></p>
<button id="friendsBtn">Friends List - %d friend(s), %d pending request(s)</button>
<p><b>Guild</b></p>
<button id="guildBtn">%s</button>
<p><b>Privacy</b></p>
<button id="privacyBtn">Settings - who can DM, friend-request, or invite you</button>
<p>&nbsp;</p>
<button id="closeBtn">Close</button>
</div>
</div>
</div>
""".formatted(escape(playerRef.getUsername()), friendCount, incoming, escape(guildLine));
String body = """
<p>Signed in as %s.</p>
<p>&nbsp;</p>
<p><b>Friends</b></p>
<button id="friendsBtn">Friends List - %d friend(s), %d pending request(s)</button>
<p>&nbsp;</p>
<p><b>Guild</b></p>
<button id="guildBtn">%s</button>
<p>&nbsp;</p>
<p><b>Privacy</b></p>
<button id="privacyBtn">Settings - who can DM, friend-request, or invite you</button>
<p>&nbsp;</p>
<button id="closeBtn" class="secondary-button">Close</button>
""".formatted(PageLayout.escape(playerRef.getUsername()), friendCount, incoming, PageLayout.escape(guildLine));
String html = PageLayout.wrap("Social", body);
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)