diff --git a/README.md b/README.md index 904f782..e139a57 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,17 @@ Permission node: `htm.command.trust` Permission node: `htm.command.trust` +### Managers + +Managers are users authorized by the owner of a protected block to manage the protection +on their behalf. Managers are able to add and remove trusted members, but cannot transfer, +remove, or change the lock type, nor can they modify flags on the container. Only the owner +can add or remove managers. + +`/htm managers add `: Allows a player to trust or untrust players on behalf of the owner. + +`/htm managers remove `: Revokes a player's access to trust or untrust players on behalf of the owner. + ### Remove `/htm remove`: Removes all protections from a container diff --git a/src/main/java/com/github/fabricservertools/htm/HTMContainerLock.java b/src/main/java/com/github/fabricservertools/htm/HTMContainerLock.java index 57506a7..9c4a23a 100644 --- a/src/main/java/com/github/fabricservertools/htm/HTMContainerLock.java +++ b/src/main/java/com/github/fabricservertools/htm/HTMContainerLock.java @@ -20,12 +20,14 @@ public class HTMContainerLock { private Lock type; private UUID owner; private HashSet trusted; + private HashSet managers; private Map flags; public HTMContainerLock() { type = null; owner = null; trusted = new HashSet<>(); + managers = new HashSet<>(); initFlags(); } @@ -51,6 +53,12 @@ public void toTag(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) tag.put("Trusted", trustedTag); + NbtList managersTag = new NbtList(); + for (UUID uuid : managers) { + managersTag.add(NbtHelper.fromUuid(uuid)); + } + tag.put("Managers", managersTag); + NbtList flagsTag = new NbtList(); for (Map.Entry entry : flags.entrySet()) { NbtCompound flagTag = new NbtCompound(); @@ -83,6 +91,12 @@ public void fromTag(NbtCompound tag, RegistryWrapper.WrapperLookup registryLooku trusted.add(NbtHelper.toUuid(value)); } + NbtList managersTag = tag.getList("Managers", NbtElement.INT_ARRAY_TYPE); + + for (NbtElement value : managersTag) { + managers.add(NbtHelper.toUuid(value)); + } + NbtList flagTags = tag.getList("Flags", NbtElement.COMPOUND_TYPE); for (NbtElement flagTag : flagTags) { NbtCompound compoundTag = (NbtCompound) flagTag; @@ -119,6 +133,8 @@ public HashSet getTrusted() { return trusted; } + public HashSet getManagers() { return managers; } + public void setType(Lock type, ServerPlayerEntity owner) { this.type = type; this.owner = owner.getUuid(); @@ -129,6 +145,7 @@ public void remove() { type = null; owner = null; trusted = new HashSet<>(); + managers = new HashSet<>(); initFlags(); } @@ -144,6 +161,12 @@ public boolean isTrusted(UUID id) { return trusted.contains(id); } + public boolean addManager(UUID id) { return managers.add(id);} + + public boolean removeManager(UUID id) { return managers.remove(id);} + + public boolean isManager(UUID id) { return managers.contains(id); } + public void transfer(UUID id) { owner = id; } diff --git a/src/main/java/com/github/fabricservertools/htm/Utility.java b/src/main/java/com/github/fabricservertools/htm/Utility.java index 66517ee..3c04d9b 100644 --- a/src/main/java/com/github/fabricservertools/htm/Utility.java +++ b/src/main/java/com/github/fabricservertools/htm/Utility.java @@ -9,8 +9,11 @@ import net.minecraft.text.Text; import net.minecraft.world.PersistentState; +import java.util.HashSet; import java.util.Optional; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; public class Utility { public static String getNameFromUUID (UUID uuid, MinecraftServer server) { @@ -34,4 +37,35 @@ public static void sendMessage(PlayerEntity player, Text message, boolean action player.sendMessage(message, actionBar); } } + + /** + * Accepts a collection of user ids and renders them as a list of usernames. + * @param userIds The unique collection of user ids. + * @param minecraftServer The Minecraft server to query for names. + * @return A comma-delimited string containing a list of names. + */ + public static String joinPlayerNames( + HashSet userIds, + MinecraftServer minecraftServer + ) { + return Utility.joinPlayerNames(userIds, minecraftServer, ", "); + } + + /** + * Accepts a collection of user ids and renders them as a list of usernames. + * @param userIds The unique collection of user ids. + * @param minecraftServer The Minecraft server to query for names. + * @param delimiter The delimiter to use when separating names. + * @return A string containing a list of names separated by {@code delimiter}. + */ + public static String joinPlayerNames( + HashSet userIds, + MinecraftServer minecraftServer, + String delimiter + ) { + return userIds + .stream() + .map(uuid -> Utility.getNameFromUUID(uuid, minecraftServer)) + .collect(Collectors.joining(delimiter)); + } } diff --git a/src/main/java/com/github/fabricservertools/htm/command/subcommands/ManagerCommands.java b/src/main/java/com/github/fabricservertools/htm/command/subcommands/ManagerCommands.java new file mode 100644 index 0000000..a30fa4e --- /dev/null +++ b/src/main/java/com/github/fabricservertools/htm/command/subcommands/ManagerCommands.java @@ -0,0 +1,65 @@ +package com.github.fabricservertools.htm.command.subcommands; + +import com.github.fabricservertools.htm.Utility; +import com.github.fabricservertools.htm.command.SubCommand; +import com.github.fabricservertools.htm.interactions.InfoAction; +import com.github.fabricservertools.htm.interactions.InteractionManager; +import com.github.fabricservertools.htm.interactions.ManagerAction; +import com.github.fabricservertools.htm.interactions.TrustAction; +import com.github.fabricservertools.htm.world.data.GlobalTrustState; +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.argument.GameProfileArgumentType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.Collection; +import java.util.stream.Collectors; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class ManagerCommands implements SubCommand { + @Override + public LiteralCommandNode build() { + return literal("managers") + .requires(Permissions.require("htm.command.info", true)) + .executes(ManagerCommands::managersList) + .then( + literal("add") + .requires(Permissions.require("htm.command.managers.add", true)) + .then(argument("target", GameProfileArgumentType.gameProfile()) + .executes(ctx -> ManagerCommands.editManagers(ctx, false))) + ) + .then( + literal("remove") + .requires(Permissions.require("htm.command.managers.remove", true)) + .then(argument("target", GameProfileArgumentType.gameProfile()) + .executes(ctx -> ManagerCommands.editManagers(ctx, true))) + ) + .build(); + } + + private static int managersList(CommandContext context) throws CommandSyntaxException { + ServerPlayerEntity player = context.getSource().getPlayer(); + + + InteractionManager.pendingActions.put(player, new InfoAction()); + context.getSource().sendFeedback(() -> Text.translatable("text.htm.select"), false); + return 1; + } + + private static int editManagers(CommandContext context, boolean untrust) throws CommandSyntaxException + { + ServerCommandSource source = context.getSource(); + Collection gameProfiles = GameProfileArgumentType.getProfileArgument(context, "target"); + + InteractionManager.pendingActions.put(source.getPlayer(), new ManagerAction(gameProfiles, untrust)); + return 1; + } +} diff --git a/src/main/java/com/github/fabricservertools/htm/interactions/InfoAction.java b/src/main/java/com/github/fabricservertools/htm/interactions/InfoAction.java index f6731e7..d1ce0cc 100644 --- a/src/main/java/com/github/fabricservertools/htm/interactions/InfoAction.java +++ b/src/main/java/com/github/fabricservertools/htm/interactions/InfoAction.java @@ -6,12 +6,15 @@ import com.github.fabricservertools.htm.Utility; import com.github.fabricservertools.htm.api.LockInteraction; import com.mojang.authlib.GameProfile; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; +import java.util.HashSet; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; public class InfoAction implements LockInteraction { @@ -33,12 +36,12 @@ public void execute(ServerPlayerEntity player, World world, BlockPos pos, HTMCon player.sendMessage(Text.translatable("text.htm.type", HTMRegistry.getLockId(lock.getType().getType()).toUpperCase()), false); player.sendMessage(Text.translatable("text.htm.owner", owner.get().getName()), false); if (lock.isOwner(player)) { - String trustedList = lock.getTrusted() - .stream() - .map(uuid -> Utility.getNameFromUUID(uuid, player.server)) - .collect(Collectors.joining(", ")); + String managersList = Utility.joinPlayerNames(lock.getManagers(), player.server); + player.sendMessage(Text.translatable("text.htm.managers", managersList), false); + String trustedList = Utility.joinPlayerNames(lock.getTrusted(), player.server); player.sendMessage(Text.translatable("text.htm.trusted", trustedList), false); + lock.getType().onInfo(player, lock); } player.sendMessage(Text.translatable("text.htm.divider"), false); diff --git a/src/main/java/com/github/fabricservertools/htm/interactions/ManagerAction.java b/src/main/java/com/github/fabricservertools/htm/interactions/ManagerAction.java new file mode 100644 index 0000000..56803a2 --- /dev/null +++ b/src/main/java/com/github/fabricservertools/htm/interactions/ManagerAction.java @@ -0,0 +1,59 @@ +package com.github.fabricservertools.htm.interactions; + +import com.github.fabricservertools.htm.HTMContainerLock; +import com.github.fabricservertools.htm.api.LockInteraction; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.Collection; +import java.util.Objects; + +public class ManagerAction implements LockInteraction { + private final Collection managers; + private final boolean untrust; + + public ManagerAction(Collection managers, boolean untrust) { + this.managers = managers; + this.untrust = untrust; + } + + @Override + public void execute(ServerPlayerEntity player, World world, BlockPos pos, HTMContainerLock lock) { + if (!lock.isLocked()) { + player.sendMessage(Text.translatable("text.htm.error.no_lock"), false); + return; + } + + // Only owners can add managers. + if (!lock.isOwner(player)) { + player.sendMessage(Text.translatable("text.htm.error.not_owner"), false); + return; + } + + for (GameProfile manager : managers) { + if (lock.getOwner() == manager.getId()) { + player.sendMessage(Text.translatable("text.htm.error.trust_self"), false); + continue; + } + + if (untrust) { + //untrust + if (lock.removeManager(manager.getId())) { + player.sendMessage(Text.translatable("text.htm.untrust_manager", manager.getName()), false); + } else { + player.sendMessage(Text.translatable("text.htm.error.manager_not_trusted", manager.getName()), false); + } + } else { + //trust + if (lock.addManager(manager.getId())) { + player.sendMessage(Text.translatable("text.htm.trust_manager", manager.getName()), false); + } else { + player.sendMessage(Text.translatable("text.htm.error.manager_already_trusted", manager.getName()), false); + } + } + } + } +} diff --git a/src/main/java/com/github/fabricservertools/htm/interactions/TrustAction.java b/src/main/java/com/github/fabricservertools/htm/interactions/TrustAction.java index 395e1a3..baeddb0 100644 --- a/src/main/java/com/github/fabricservertools/htm/interactions/TrustAction.java +++ b/src/main/java/com/github/fabricservertools/htm/interactions/TrustAction.java @@ -26,7 +26,8 @@ public void execute(ServerPlayerEntity player, World world, BlockPos pos, HTMCon return; } - if (!lock.isOwner(player)) { + // Players and managers can manage trustees. + if (!lock.isOwner(player) && !lock.isManager(player.getUuid())) { player.sendMessage(Text.translatable("text.htm.error.not_owner"), false); return; } diff --git a/src/main/java/com/github/fabricservertools/htm/locks/KeyLock.java b/src/main/java/com/github/fabricservertools/htm/locks/KeyLock.java index 919f174..ed7d8bf 100644 --- a/src/main/java/com/github/fabricservertools/htm/locks/KeyLock.java +++ b/src/main/java/com/github/fabricservertools/htm/locks/KeyLock.java @@ -24,6 +24,7 @@ public class KeyLock implements Lock { @Override public boolean canOpen(ServerPlayerEntity player, HTMContainerLock lock) { if (lock.isTrusted(player.getUuid())) return true; + if (lock.isManager(player.getUuid())) return true; if (Utility.getGlobalTrustState(player.server).isTrusted(lock.getOwner(), player.getUuid())) return true; diff --git a/src/main/java/com/github/fabricservertools/htm/locks/PrivateLock.java b/src/main/java/com/github/fabricservertools/htm/locks/PrivateLock.java index 285301d..5f5d82b 100644 --- a/src/main/java/com/github/fabricservertools/htm/locks/PrivateLock.java +++ b/src/main/java/com/github/fabricservertools/htm/locks/PrivateLock.java @@ -12,6 +12,7 @@ public class PrivateLock implements Lock { @Override public boolean canOpen(ServerPlayerEntity player, HTMContainerLock lock) { if (lock.isTrusted(player.getUuid())) return true; + if (lock.isManager(player.getUuid())) return true; return Utility.getGlobalTrustState(player.server).isTrusted(lock.getOwner(), player.getUuid()); } diff --git a/src/main/resources/data/htm/lang/en_us.json b/src/main/resources/data/htm/lang/en_us.json index f4e6a6a..3ecec73 100644 --- a/src/main/resources/data/htm/lang/en_us.json +++ b/src/main/resources/data/htm/lang/en_us.json @@ -30,5 +30,11 @@ "text.htm.persist": "§bToggled persist ", "text.htm.no_msg": "§bToggled no messages ", "text.htm.on": "§a§lOn", - "text.htm.off": "§c§lOff" -} \ No newline at end of file + "text.htm.off": "§c§lOff", + + "text.htm.trust_manager": "§bAdded Manager: §a%s", + "text.htm.untrust_manager": "§bRemoved Manager: §a%s", + "text.htm.error.manager_not_trusted": "§r§a%s §cisn't a manager", + "text.htm.error.manager_already_trusted": "§r§a%s §cis already a manager", + "text.htm.managers": "§b§lManagers: §r§a%s" +}