replace with toggle + add aliases

This commit is contained in:
2026-05-29 13:29:34 -04:00
parent b506724829
commit 090bf65d2e
9 changed files with 235 additions and 98 deletions
@@ -14,6 +14,7 @@ import net.kewwbec.social.command.dm.ChatCommand;
import net.kewwbec.social.command.dm.MsgCommand; import net.kewwbec.social.command.dm.MsgCommand;
import net.kewwbec.social.command.dm.ReplyCommand; import net.kewwbec.social.command.dm.ReplyCommand;
import net.kewwbec.social.command.friend.FriendCommand; import net.kewwbec.social.command.friend.FriendCommand;
import net.kewwbec.social.command.friend.FriendListCommand;
import net.kewwbec.social.command.guild.GuildCommand; import net.kewwbec.social.command.guild.GuildCommand;
import net.kewwbec.social.command.privacy.PrivacyCommand; import net.kewwbec.social.command.privacy.PrivacyCommand;
import net.kewwbec.social.config.SocialConfig; import net.kewwbec.social.config.SocialConfig;
@@ -106,6 +107,13 @@ public final class SocialPlugin extends JavaPlugin {
FriendService friends = new FriendService(friendRepo, privacy, bus, config, core.getServerId()); FriendService friends = new FriendService(friendRepo, privacy, bus, config, core.getServerId());
GuildService guilds = new GuildService(guildRepo, rankRepo, memberRepo, inviteRepo, privacy, bus, config, core.getServerId()); GuildService guilds = new GuildService(guildRepo, rankRepo, memberRepo, inviteRepo, privacy, bus, config, core.getServerId());
core.registerService(net.kewwbec.networkcore.api.guilds.GuildLookup.class, uuid -> {
GuildService.GuildContext gc = guilds.contextFor(uuid);
if (gc == null) return null;
return new net.kewwbec.networkcore.api.guilds.GuildInfo(
gc.guild().id(), gc.guild().name(), gc.guild().tag(), gc.rank().name());
});
this.guildChat = new GuildChatService(bus, memberRepo, config, core.getServerId()); this.guildChat = new GuildChatService(bus, memberRepo, config, core.getServerId());
guildChat.start(); guildChat.start();
@@ -123,9 +131,10 @@ public final class SocialPlugin extends JavaPlugin {
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, disconnect::onDisconnect); getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, disconnect::onDisconnect);
getCommandRegistry().registerCommand(new FriendCommand(friends, resolver)); getCommandRegistry().registerCommand(new FriendCommand(friends, resolver));
getCommandRegistry().registerCommand(new FriendListCommand(friends));
getCommandRegistry().registerCommand(new GuildCommand(guilds, resolver, chatTargets)); getCommandRegistry().registerCommand(new GuildCommand(guilds, resolver, chatTargets));
getCommandRegistry().registerCommand(new MsgCommand(chatTargets, resolver, dms)); getCommandRegistry().registerCommand(new MsgCommand(resolver, dms));
getCommandRegistry().registerCommand(new ReplyCommand(chatTargets, dms, presence)); getCommandRegistry().registerCommand(new ReplyCommand(dms, presence));
getCommandRegistry().registerCommand(new ChatCommand(chatTargets)); getCommandRegistry().registerCommand(new ChatCommand(chatTargets));
getCommandRegistry().registerCommand(new PrivacyCommand(privacy)); getCommandRegistry().registerCommand(new PrivacyCommand(privacy));
getCommandRegistry().registerCommand(new SocialCommand(new SocialUIContext(friends, guilds, privacy))); getCommandRegistry().registerCommand(new SocialCommand(new SocialUIContext(friends, guilds, privacy)));
@@ -4,7 +4,6 @@ import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.CommandContext; 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.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; 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.command.system.basecommands.AbstractPlayerCommand;
@@ -12,8 +11,6 @@ import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.networkcore.api.PlayerLocation; 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.service.DmRoutingService;
import net.kewwbec.social.util.PlayerResolver; import net.kewwbec.social.util.PlayerResolver;
@@ -21,28 +18,20 @@ import javax.annotation.Nonnull;
import java.awt.Color; import java.awt.Color;
/** /**
* /msg <name> [message...] * /msg <name> <message...> - send a DM to an online player.
*
* 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 { public final class MsgCommand extends AbstractPlayerCommand {
private final ChatTargetService targets;
private final PlayerResolver resolver; private final PlayerResolver resolver;
private final DmRoutingService dms; private final DmRoutingService dms;
private final RequiredArg<String> nameArg = withRequiredArg("name", "Recipient", ArgTypes.STRING); private final RequiredArg<String> nameArg = withRequiredArg("name", "Recipient", ArgTypes.STRING);
private final OptionalArg<String> bodyArg = withOptionalArg("message", "Message body (optional)", ArgTypes.GREEDY_STRING); private final RequiredArg<String> bodyArg = withRequiredArg("message", "Message body", ArgTypes.GREEDY_STRING);
public MsgCommand(@Nonnull ChatTargetService targets, public MsgCommand(@Nonnull PlayerResolver resolver, @Nonnull DmRoutingService dms) {
@Nonnull PlayerResolver resolver, super("msg", "Send a DM to a player");
@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.resolver = resolver;
this.dms = dms; this.dms = dms;
requirePermission(SocialPermissionsNodes.DM_USE); // requirePermission(SocialPermissionsNodes.DM_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -50,8 +39,9 @@ public final class MsgCommand extends AbstractPlayerCommand {
@Override @Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) { 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); String name = nameArg.get(ctx);
if (name == null) { String body = bodyArg.get(ctx);
ctx.sendMessage(Message.raw("Usage: /msg <name> [message...]").color(Color.YELLOW)); if (name == null || body == null || body.isBlank()) {
ctx.sendMessage(Message.raw("Usage: /msg <name> <message...>").color(Color.YELLOW));
return; return;
} }
PlayerLocation target = resolver.resolve(name); PlayerLocation target = resolver.resolve(name);
@@ -59,18 +49,10 @@ public final class MsgCommand extends AbstractPlayerCommand {
ctx.sendMessage(Message.raw(name + " is not online.").color(Color.RED)); ctx.sendMessage(Message.raw(name + " is not online.").color(Color.RED));
return; return;
} }
DmRoutingService.Result r = dms.send(sender, target.username(), body.trim());
String body = bodyArg.get(ctx); if (r != DmRoutingService.Result.OK) {
if (body != null && !body.isBlank()) { ctx.sendMessage(Message.raw(errorText(r, target.username())).color(Color.RED));
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 @Nonnull
@@ -4,7 +4,7 @@ import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.CommandContext; 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.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; 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.PlayerRef;
@@ -12,8 +12,6 @@ import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.social.SocialPermissionsNodes;
import net.kewwbec.social.service.ChatTargetService;
import net.kewwbec.social.service.DmRoutingService; import net.kewwbec.social.service.DmRoutingService;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -21,33 +19,30 @@ import java.awt.Color;
import java.util.UUID; import java.util.UUID;
/** /**
* /r [message...] * /r &lt;message...&gt; - send a DM to whoever DMed you last.
*
* 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 { public final class ReplyCommand extends AbstractPlayerCommand {
private final ChatTargetService targets;
private final DmRoutingService dms; private final DmRoutingService dms;
private final PlayerPresence presence; private final PlayerPresence presence;
private final OptionalArg<String> bodyArg = withOptionalArg("message", "Message body (optional)", ArgTypes.GREEDY_STRING); private final RequiredArg<String> bodyArg = withRequiredArg("message", "Message body", ArgTypes.GREEDY_STRING);
public ReplyCommand(@Nonnull ChatTargetService targets, public ReplyCommand(@Nonnull DmRoutingService dms, @Nonnull PlayerPresence presence) {
@Nonnull DmRoutingService dms, super("r", "Reply to your last DM sender");
@Nonnull PlayerPresence presence) {
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.dms = dms;
this.presence = presence; this.presence = presence;
requirePermission(SocialPermissionsNodes.DM_USE); // requirePermission(SocialPermissionsNodes.DM_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) { protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
String body = bodyArg.get(ctx);
if (body == null || body.isBlank()) {
ctx.sendMessage(Message.raw("Usage: /r <message...>").color(Color.YELLOW));
return;
}
UUID last = dms.getLastSender(sender.getUuid()); UUID last = dms.getLastSender(sender.getUuid());
if (last == null) { if (last == null) {
ctx.sendMessage(Message.raw("No one has DMed you recently.").color(Color.RED)); ctx.sendMessage(Message.raw("No one has DMed you recently.").color(Color.RED));
@@ -58,18 +53,10 @@ public final class ReplyCommand extends AbstractPlayerCommand {
ctx.sendMessage(Message.raw("That person is offline.").color(Color.RED)); ctx.sendMessage(Message.raw("That person is offline.").color(Color.RED));
return; return;
} }
DmRoutingService.Result r = dms.send(sender, target.username(), body.trim());
String body = bodyArg.get(ctx); if (r != DmRoutingService.Result.OK) {
if (body != null && !body.isBlank()) { ctx.sendMessage(Message.raw(errorText(r, target.username())).color(Color.RED));
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 @Nonnull
@@ -11,7 +11,11 @@ import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayer
import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.networkcore.NetworkCore;
import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.ranks.RankInfo;
import net.kewwbec.networkcore.api.ranks.RankLookup;
import net.kewwbec.social.SocialPermissionsNodes; import net.kewwbec.social.SocialPermissionsNodes;
import net.kewwbec.social.model.Friendship; import net.kewwbec.social.model.Friendship;
import net.kewwbec.social.service.FriendService; import net.kewwbec.social.service.FriendService;
@@ -27,6 +31,7 @@ public final class FriendCommand extends AbstractCommandCollection {
public FriendCommand(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) { public FriendCommand(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
super("friend", "Manage friends (add, accept, reject, remove, list)"); super("friend", "Manage friends (add, accept, reject, remove, list)");
addAliases("f");
addSubCommand(new Add(service, resolver)); addSubCommand(new Add(service, resolver));
addSubCommand(new Accept(service, resolver)); addSubCommand(new Accept(service, resolver));
addSubCommand(new Reject(service, resolver)); addSubCommand(new Reject(service, resolver));
@@ -46,7 +51,7 @@ public final class FriendCommand extends AbstractCommandCollection {
super("add", "Send a friend request"); super("add", "Send a friend request");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -83,7 +88,7 @@ public final class FriendCommand extends AbstractCommandCollection {
super("accept", "Accept an incoming friend request"); super("accept", "Accept an incoming friend request");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -118,7 +123,7 @@ public final class FriendCommand extends AbstractCommandCollection {
super("reject", "Reject an incoming friend request"); super("reject", "Reject an incoming friend request");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -151,7 +156,7 @@ public final class FriendCommand extends AbstractCommandCollection {
super("remove", "Remove a friend"); super("remove", "Remove a friend");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -185,24 +190,78 @@ public final class FriendCommand extends AbstractCommandCollection {
public ListAll(@Nonnull FriendService service) { public ListAll(@Nonnull FriendService service) {
super("list", "Show your friends"); super("list", "Show your friends");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) { protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
render(ctx, service, sender);
}
public static void render(@Nonnull CommandContext ctx, @Nonnull FriendService service, @Nonnull PlayerRef sender) {
try { try {
List<Friendship> all = service.list(sender.getUuid()); List<Friendship> all = service.list(sender.getUuid());
if (all.isEmpty()) { ctx.sendMessage(Message.raw("You have no friends yet.").color(Color.GRAY)); return; } if (all.isEmpty()) { ctx.sendMessage(Message.raw("You have no friends yet.").color(Color.GRAY)); return; }
ctx.sendMessage(Message.raw("=== Friends (" + all.size() + ") ===").color(Color.YELLOW));
PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence();
RankLookup ranks = NetworkCore.getInstance().getRankLookup();
int online = 0;
for (Friendship f : all) { for (Friendship f : all) {
String other = f.fromUuid().equals(sender.getUuid()) ? f.toName() : f.fromName(); UUID otherUuid = f.fromUuid().equals(sender.getUuid()) ? f.toUuid() : f.fromUuid();
ctx.sendMessage(Message.raw("- " + other).color(Color.WHITE)); if (presence != null && presence.getLocation(otherUuid) != null) online++;
}
ctx.sendMessage(Message.raw("=== Friends (" + online + "/" + all.size() + " online) ===").color(Color.YELLOW));
for (Friendship f : all) {
UUID otherUuid = f.fromUuid().equals(sender.getUuid()) ? f.toUuid() : f.fromUuid();
String otherName = f.fromUuid().equals(sender.getUuid()) ? f.toName() : f.fromName();
PlayerLocation loc = (presence == null) ? null : presence.getLocation(otherUuid);
String status = (loc == null) ? "offline" : "online on " + loc.serverId();
RankInfo rank = (ranks == null) ? null : safeLookup(ranks, otherUuid);
ctx.sendMessage(buildRow(rank, otherName, status, loc != null));
} }
} catch (SQLException e) { } catch (SQLException e) {
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED)); ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
} }
} }
@Nonnull
private static Message buildRow(@javax.annotation.Nullable RankInfo rank,
@Nonnull String name,
@Nonnull String status,
boolean isOnline) {
String prefix = (rank == null || rank.prefix() == null || rank.prefix().isBlank())
? "" : rank.prefix() + " ";
Color rankColor = (rank == null) ? null : parseHex(rank.color());
Color statusColor = isOnline ? Color.GREEN : Color.GRAY;
Message line = Message.empty();
if (!prefix.isEmpty()) {
Message prefixMsg = Message.raw(prefix);
line = line.insert(rankColor == null ? prefixMsg.color(Color.WHITE) : prefixMsg.color(rankColor));
}
Message nameMsg = Message.raw(name);
line = line.insert(rankColor == null ? nameMsg.color(Color.WHITE) : nameMsg.color(rankColor));
line = line.insert(Message.raw(" - " + status).color(statusColor));
return line;
}
@javax.annotation.Nullable
private static RankInfo safeLookup(@Nonnull RankLookup ranks, @Nonnull UUID uuid) {
try { return ranks.getPrimaryRank(uuid); }
catch (RuntimeException ignored) { return null; }
}
@javax.annotation.Nullable
private static Color parseHex(@javax.annotation.Nullable String hex) {
if (hex == null || hex.isBlank()) return null;
String s = hex.startsWith("#") ? hex.substring(1) : hex;
if (s.length() != 6) return null;
try { return new Color(Integer.parseInt(s, 16)); }
catch (NumberFormatException ignored) { return null; }
}
} }
public static final class Pending extends AbstractPlayerCommand { public static final class Pending extends AbstractPlayerCommand {
@@ -211,7 +270,7 @@ public final class FriendCommand extends AbstractCommandCollection {
public Pending(@Nonnull FriendService service) { public Pending(@Nonnull FriendService service) {
super("pending", "Show pending friend requests (incoming + outgoing)"); super("pending", "Show pending friend requests (incoming + outgoing)");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.FRIEND_USE); // requirePermission(SocialPermissionsNodes.FRIEND_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -0,0 +1,36 @@
package net.kewwbec.social.command.friend;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
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;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.social.service.FriendService;
import javax.annotation.Nonnull;
/**
* /fl - top-level alias of /friend list. Same render logic via {@link FriendCommand.ListAll#render}.
*/
public final class FriendListCommand extends AbstractPlayerCommand {
private final FriendService service;
public FriendListCommand(@Nonnull FriendService service) {
super("fl", "Show your friends (alias of /friend list)");
this.service = service;
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef sender,
@Nonnull World world) {
FriendCommand.ListAll.render(ctx, service, sender);
}
}
@@ -32,6 +32,7 @@ public final class GuildCommand extends AbstractCommandCollection {
@Nonnull PlayerResolver resolver, @Nonnull PlayerResolver resolver,
@Nonnull ChatTargetService chatTargets) { @Nonnull ChatTargetService chatTargets) {
super("guild", "Guild commands: create, invite, leave, etc."); super("guild", "Guild commands: create, invite, leave, etc.");
addAliases("g");
addSubCommand(new Create(service)); addSubCommand(new Create(service));
addSubCommand(new Disband(service)); addSubCommand(new Disband(service));
addSubCommand(new Invite(service, resolver)); addSubCommand(new Invite(service, resolver));
@@ -56,7 +57,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Create(@Nonnull GuildService service) { public Create(@Nonnull GuildService service) {
super("create", "Create a new guild"); super("create", "Create a new guild");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_CREATE); // requirePermission(SocialPermissionsNodes.GUILD_CREATE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -80,7 +81,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Disband(@Nonnull GuildService service) { public Disband(@Nonnull GuildService service) {
super("disband", "Disband your guild (owner only)"); super("disband", "Disband your guild (owner only)");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -104,7 +105,7 @@ public final class GuildCommand extends AbstractCommandCollection {
super("invite", "Invite a player to your guild"); super("invite", "Invite a player to your guild");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -136,7 +137,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Accept(@Nonnull GuildService service) { public Accept(@Nonnull GuildService service) {
super("accept", "Accept a pending guild invite"); super("accept", "Accept a pending guild invite");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -162,7 +163,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Leave(@Nonnull GuildService service) { public Leave(@Nonnull GuildService service) {
super("leave", "Leave your current guild"); super("leave", "Leave your current guild");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -186,7 +187,7 @@ public final class GuildCommand extends AbstractCommandCollection {
super("kick", "Kick a guild member"); super("kick", "Kick a guild member");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -221,7 +222,7 @@ public final class GuildCommand extends AbstractCommandCollection {
super("promote", "Promote a member to a higher rank"); super("promote", "Promote a member to a higher rank");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -258,7 +259,7 @@ public final class GuildCommand extends AbstractCommandCollection {
super("demote", "Demote a member to a lower rank"); super("demote", "Demote a member to a lower rank");
this.service = service; this.service = service;
this.resolver = resolver; this.resolver = resolver;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -292,7 +293,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Motd(@Nonnull GuildService service) { public Motd(@Nonnull GuildService service) {
super("motd", "Set the guild MOTD"); super("motd", "Set the guild MOTD");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -315,7 +316,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public Info(@Nonnull GuildService service) { public Info(@Nonnull GuildService service) {
super("info", "Show your guild"); super("info", "Show your guild");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -334,7 +335,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public RosterCmd(@Nonnull GuildService service) { public RosterCmd(@Nonnull GuildService service) {
super("roster", "Show all guild members"); super("roster", "Show all guild members");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -358,7 +359,7 @@ public final class GuildCommand extends AbstractCommandCollection {
public RanksCmd(@Nonnull GuildService service) { public RanksCmd(@Nonnull GuildService service) {
super("ranks", "Show your guild's ranks"); super("ranks", "Show your guild's ranks");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -388,7 +389,7 @@ public final class GuildCommand extends AbstractCommandCollection {
super("chat", "Toggle guild chat mode (your chat goes to your guild)"); super("chat", "Toggle guild chat mode (your chat goes to your guild)");
this.service = service; this.service = service;
this.chatTargets = chatTargets; this.chatTargets = chatTargets;
requirePermission(SocialPermissionsNodes.GUILD_USE); // requirePermission(SocialPermissionsNodes.GUILD_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@Override @Override
@@ -36,7 +36,7 @@ public final class PrivacyCommand extends AbstractCommandCollection {
public Show(@Nonnull PrivacyService service) { public Show(@Nonnull PrivacyService service) {
super("show", "Show your current privacy settings"); super("show", "Show your current privacy settings");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.PRIVACY_USE); // requirePermission(SocialPermissionsNodes.PRIVACY_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -63,7 +63,7 @@ public final class PrivacyCommand extends AbstractCommandCollection {
public SetCmd(@Nonnull PrivacyService service) { public SetCmd(@Nonnull PrivacyService service) {
super("set", "Change one of your privacy settings"); super("set", "Change one of your privacy settings");
this.service = service; this.service = service;
requirePermission(SocialPermissionsNodes.PRIVACY_USE); // requirePermission(SocialPermissionsNodes.PRIVACY_USE);
} }
@Override protected boolean canGeneratePermission() { return false; } @Override protected boolean canGeneratePermission() { return false; }
@@ -4,9 +4,12 @@ import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.Universe; import com.hypixel.hytale.server.core.universe.Universe;
import net.kewwbec.networkcore.NetworkCore;
import net.kewwbec.networkcore.api.MessageBus; import net.kewwbec.networkcore.api.MessageBus;
import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.ranks.RankInfo;
import net.kewwbec.networkcore.api.ranks.RankLookup;
import net.kewwbec.social.config.SocialConfig; import net.kewwbec.social.config.SocialConfig;
import net.kewwbec.social.model.PrivacyKey; import net.kewwbec.social.model.PrivacyKey;
import net.kewwbec.social.service.payload.DmPayload; import net.kewwbec.social.service.payload.DmPayload;
@@ -21,10 +24,10 @@ import java.util.logging.Level;
/** /**
* Cross-server DM routing. * Cross-server DM routing.
* *
* Sender side: validate privacy, then publish a DmPayload on the dm channel. * Each delivered/echoed line is composed as: [optional colored rank prefix] [from/to NAME] body.
* Each server's subscription checks whether the recipient is on its local universe; if so, it * The rank prefix uses the SENDER's primary rank (so the recipient sees who, with rank, sent it,
* delivers the message; otherwise it ignores. This means recipient privacy is checked on the * and the sender's echo also shows their own prefix as confirmation). If no RankLookup service is
* sender's server. * registered (e.g. Ranks plugin not installed) or the sender has no rank, the prefix is omitted.
*/ */
public final class DmRoutingService { public final class DmRoutingService {
@@ -90,12 +93,12 @@ public final class DmRoutingService {
body, serverId); body, serverId);
bus.publish(config.channels.dm, p); bus.publish(config.channels.dm, p);
// Local echo to the sender
Universe u = Universe.get(); Universe u = Universe.get();
if (u != null) { if (u != null) {
RankInfo recipientRank = lookupRank(target.uuid());
for (PlayerRef ref : u.getPlayers()) { for (PlayerRef ref : u.getPlayers()) {
if (ref.getUuid().equals(sender.getUuid())) { if (ref.getUuid().equals(sender.getUuid())) {
ref.sendMessage(Message.raw(formatTo(target.username(), body)).color(Color.LIGHT_GRAY)); ref.sendMessage(buildLine("To", recipientRank, target.username(), body));
break; break;
} }
} }
@@ -126,19 +129,56 @@ public final class DmRoutingService {
} }
for (PlayerRef ref : u.getPlayers()) { for (PlayerRef ref : u.getPlayers()) {
if (!ref.getUuid().equals(recipientUuid)) continue; if (!ref.getUuid().equals(recipientUuid)) continue;
ref.sendMessage(Message.raw(formatFrom(p.senderName, p.body)).color(Color.WHITE)); RankInfo senderRank = lookupRank(senderUuid);
ref.sendMessage(buildLine("From", senderRank, p.senderName, p.body));
lastSender.put(recipientUuid, senderUuid); lastSender.put(recipientUuid, senderUuid);
return; return;
} }
} }
@Nonnull @Nullable
private String formatTo(@Nonnull String name, @Nonnull String body) { private RankInfo lookupRank(@Nonnull UUID uuid) {
return config.dms.prefix_to.replace("{name}", name) + " " + body; try {
RankLookup ranks = NetworkCore.getInstance().getRankLookup();
return ranks == null ? null : ranks.getPrimaryRank(uuid);
} catch (RuntimeException ignored) {
return null;
}
} }
private static final Color GOLD = new Color(0xFFAA00);
@Nonnull @Nonnull
private String formatFrom(@Nonnull String name, @Nonnull String body) { private static Message buildLine(@Nonnull String leadWord,
return config.dms.prefix_from.replace("{name}", name) + " " + body; @Nullable RankInfo otherRank,
@Nonnull String otherName,
@Nonnull String body) {
String prefix = (otherRank == null || otherRank.prefix() == null || otherRank.prefix().isBlank())
? "" : otherRank.prefix();
Color nameColor = (otherRank == null) ? null : parseHex(otherRank.color());
Message line = Message.empty()
.insert(Message.raw(leadWord + " ").color(GOLD));
if (!prefix.isEmpty()) {
Message prefixMsg = Message.raw(prefix + " ");
line = line.insert(nameColor == null ? prefixMsg : prefixMsg.color(nameColor));
}
Message nameMsg = Message.raw(otherName);
line = line.insert(nameColor == null ? nameMsg : nameMsg.color(nameColor));
line = line.insert(Message.raw(": ").color(GOLD));
line = line.insert(Message.raw(body).color(Color.WHITE));
return line;
}
@Nullable
private static Color parseHex(@Nullable String hex) {
if (hex == null || hex.isBlank()) return null;
String s = hex.startsWith("#") ? hex.substring(1) : hex;
if (s.length() != 6) return null;
try {
return new Color(Integer.parseInt(s, 16));
} catch (NumberFormatException ignored) {
return null;
}
} }
} }
@@ -11,6 +11,8 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.networkcore.NetworkCore; import net.kewwbec.networkcore.NetworkCore;
import net.kewwbec.networkcore.api.PlayerLocation; import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.ranks.RankInfo;
import net.kewwbec.networkcore.api.ranks.RankLookup;
import net.kewwbec.social.model.Friendship; import net.kewwbec.social.model.Friendship;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -99,13 +101,15 @@ public final class FriendsListPage {
return sb.toString(); return sb.toString();
} }
PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence(); PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence();
RankLookup ranks = NetworkCore.getInstance().getRankLookup();
int i = 0; int i = 0;
for (Friendship f : friends) { for (Friendship f : friends) {
UUID otherUuid = f.fromUuid().equals(playerRef.getUuid()) ? f.toUuid() : f.fromUuid(); UUID otherUuid = f.fromUuid().equals(playerRef.getUuid()) ? f.toUuid() : f.fromUuid();
String otherName = f.fromUuid().equals(playerRef.getUuid()) ? f.toName() : f.fromName(); String otherName = f.fromUuid().equals(playerRef.getUuid()) ? f.toName() : f.fromName();
PlayerLocation loc = (presence == null) ? null : presence.getLocation(otherUuid); PlayerLocation loc = (presence == null) ? null : presence.getLocation(otherUuid);
String status = (loc == null) ? "offline" : "on " + loc.serverId(); String status = (loc == null) ? "offline" : "online on " + loc.serverId();
sb.append("<p>").append(escape(otherName)).append(" (").append(escape(status)).append(")</p>"); RankInfo rank = (ranks == null) ? null : safeLookup(ranks, otherUuid);
sb.append(renderNameRow(rank, otherName, status));
String removeId = "removeFriend" + i; String removeId = "removeFriend" + i;
sb.append("<button id=\"").append(removeId).append("\" class=\"secondary-button\">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; UUID finalOther = otherUuid;
@@ -118,6 +122,21 @@ public final class FriendsListPage {
return sb.toString(); return sb.toString();
} }
@Nonnull
private static String renderNameRow(@javax.annotation.Nullable RankInfo rank, @Nonnull String name, @Nonnull String status) {
String prefix = (rank == null || rank.prefix() == null || rank.prefix().isBlank()) ? "" : rank.prefix() + " ";
String hex = (rank == null) ? null : rank.color();
String styleAttr = (hex == null || hex.isBlank()) ? "" : " style=\"color: " + escape(hex) + ";\"";
return "<p" + styleAttr + ">" + escape(prefix) + escape(name) + "</p>"
+ "<p>" + escape(status) + "</p>";
}
@javax.annotation.Nullable
private static RankInfo safeLookup(@Nonnull RankLookup ranks, @Nonnull UUID uuid) {
try { return ranks.getPrimaryRank(uuid); }
catch (RuntimeException e) { return null; }
}
@Nonnull @Nonnull
private String renderIncoming(@Nonnull List<RowAction> actions) throws SQLException { private String renderIncoming(@Nonnull List<RowAction> actions) throws SQLException {
List<Friendship> incoming = ctx.friends.incomingPending(playerRef.getUuid()); List<Friendship> incoming = ctx.friends.incomingPending(playerRef.getUuid());
@@ -127,11 +146,13 @@ public final class FriendsListPage {
sb.append("<p>No one has sent you a friend request.</p>"); sb.append("<p>No one has sent you a friend request.</p>");
return sb.toString(); return sb.toString();
} }
RankLookup ranks = NetworkCore.getInstance().getRankLookup();
int i = 0; int i = 0;
for (Friendship f : incoming) { for (Friendship f : incoming) {
UUID fromUuid = f.fromUuid(); UUID fromUuid = f.fromUuid();
String fromName = f.fromName(); String fromName = f.fromName();
sb.append("<p>from ").append(escape(fromName)).append("</p>"); RankInfo rank = (ranks == null) ? null : safeLookup(ranks, fromUuid);
sb.append(renderNameRow(rank, fromName, "wants to be your friend"));
String acceptId = "accept" + i; String acceptId = "accept" + i;
String rejectId = "reject" + i; String rejectId = "reject" + i;
sb.append("<button id=\"").append(acceptId).append("\">Accept ").append(escape(fromName)).append("</button>"); sb.append("<button id=\"").append(acceptId).append("\">Accept ").append(escape(fromName)).append("</button>");
@@ -156,11 +177,13 @@ public final class FriendsListPage {
sb.append("<p>No outgoing requests.</p>"); sb.append("<p>No outgoing requests.</p>");
return sb.toString(); return sb.toString();
} }
RankLookup ranks = NetworkCore.getInstance().getRankLookup();
int i = 0; int i = 0;
for (Friendship f : outgoing) { for (Friendship f : outgoing) {
UUID toUuid = f.toUuid(); UUID toUuid = f.toUuid();
String toName = f.toName(); String toName = f.toName();
sb.append("<p>to ").append(escape(toName)).append("</p>"); RankInfo rank = (ranks == null) ? null : safeLookup(ranks, toUuid);
sb.append(renderNameRow(rank, toName, "waiting for them to accept"));
String cancelId = "cancel" + i; String cancelId = "cancel" + i;
sb.append("<button id=\"").append(cancelId).append("\" class=\"secondary-button\">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, () -> actions.add(new RowAction(cancelId, () ->