diff --git a/ClaimLink/ClaimLink.cs b/ClaimLink/ClaimLink.cs index 9e55ed2..2ba704d 100644 --- a/ClaimLink/ClaimLink.cs +++ b/ClaimLink/ClaimLink.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using ProtoBuf; +using Vintagestory.API.Common; namespace ClaimLink; @@ -11,6 +12,22 @@ public class ClaimLinkSaveData [ProtoMember(2)] public List Entries = new(); + + [ProtoMember(3)] + public List Permissions = new(); +} + +[ProtoContract] +public class ClaimLinkPermissionSaveData +{ + [ProtoMember(1)] + public int GroupId; + + [ProtoMember(2)] + public Dictionary PermittedPlayerUids = new(); + + [ProtoMember(3)] + public Dictionary PermittedPlayerGroupIds = new(); } [ProtoContract] diff --git a/ClaimLink/ClaimLinkBufferProtection.cs b/ClaimLink/ClaimLinkBufferProtection.cs index 9e5a201..ede7d99 100644 --- a/ClaimLink/ClaimLinkBufferProtection.cs +++ b/ClaimLink/ClaimLinkBufferProtection.cs @@ -40,7 +40,10 @@ internal static class ClaimLinkBufferProtection continue; foreach (Cuboidi buffer in ClaimLinkBufferIndex.CachedBuffer(groupId)) - if (buffer.Contains(pos)) + if ( + buffer.Contains(pos) + && !ClaimLinkModSystem.Registry.HasPermission(groupId, byPlayer, accessFlag) + ) allowed = false; } diff --git a/ClaimLink/ClaimLinkChatCommand.cs b/ClaimLink/ClaimLinkChatCommand.cs index 20ea680..db989fa 100644 --- a/ClaimLink/ClaimLinkChatCommand.cs +++ b/ClaimLink/ClaimLinkChatCommand.cs @@ -113,6 +113,44 @@ public static class ClaimLinkChatCommand true, Kick ), + new( + new[] { "allow" }, + "Grant a player access inside the claim link's buffer zone.", + new ICommandArgumentParser[] + { + p.Word("playername"), + p.WordRange("access", "use", "build", "all"), + p.OptionalWord("groupname"), + }, + true, + Allow + ), + new( + new[] { "allowgroup" }, + "Grant a player group access inside the claim link's buffer zone.", + new ICommandArgumentParser[] + { + p.Word("targetgroup"), + p.WordRange("access", "use", "build", "all"), + p.OptionalWord("groupname"), + }, + true, + AllowGroup + ), + new( + new[] { "deny" }, + "Revoke a player's buffer-zone access.", + new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") }, + true, + Deny + ), + new( + new[] { "denygroup" }, + "Revoke a player group's buffer-zone access.", + new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("targetgroup") }, + true, + DenyGroup + ), new( new[] { "delete" }, "Delete a claim link.", @@ -184,6 +222,44 @@ public static class ClaimLinkChatCommand false, AdminTransferOwnership ), + new( + new[] { "allow" }, + "Grant a player access inside any claim link's buffer zone.", + new ICommandArgumentParser[] + { + p.Word("playername"), + p.WordRange("access", "use", "build", "all"), + p.OptionalWord("groupname"), + }, + false, + AdminAllow + ), + new( + new[] { "allowgroup" }, + "Grant a player group access inside any claim link's buffer zone.", + new ICommandArgumentParser[] + { + p.Word("targetgroup"), + p.WordRange("access", "use", "build", "all"), + p.OptionalWord("groupname"), + }, + false, + AdminAllowGroup + ), + new( + new[] { "deny" }, + "Revoke a player's buffer-zone access to any claim link.", + new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") }, + false, + AdminDeny + ), + new( + new[] { "denygroup" }, + "Revoke a player group's buffer-zone access to any claim link.", + new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("targetgroup") }, + false, + AdminDenyGroup + ), }; foreach (var spec in adminCommands) @@ -532,6 +608,180 @@ public static class ClaimLinkChatCommand ); } + private static EnumBlockAccessFlags ParseAccess(string word) => + word switch + { + "use" => EnumBlockAccessFlags.Use, + "build" => EnumBlockAccessFlags.BuildOrBreak, + _ => EnumBlockAccessFlags.Use | EnumBlockAccessFlags.BuildOrBreak, + }; + + private static TextCommandResult ExecuteAllow( + PlayerGroup group, + string targetName, + string accessWord + ) + { + string? targetUid = ClaimLinkModSystem + .PlayerData.GetPlayerDataByLastKnownName(targetName) + ?.PlayerUID; + if (targetUid == null) + return TextCommandResult.Error($"No such player '{targetName}'."); + + ClaimLinkModSystem.Registry.GrantPlayer(group.Uid, targetUid, ParseAccess(accessWord)); + return TextCommandResult.Success( + $"Granted {targetName} {accessWord} access to '{group.Name}''s buffer." + ); + } + + private static TextCommandResult ExecuteDeny(PlayerGroup group, string targetName) + { + string? targetUid = ClaimLinkModSystem + .PlayerData.GetPlayerDataByLastKnownName(targetName) + ?.PlayerUID; + if (targetUid == null) + return TextCommandResult.Error($"No such player '{targetName}'."); + + return ClaimLinkModSystem.Registry.RevokePlayer(group.Uid, targetUid) + ? TextCommandResult.Success($"Revoked {targetName}'s buffer access to '{group.Name}'.") + : TextCommandResult.Error($"{targetName} has no buffer access granted in '{group.Name}'."); + } + + private static TextCommandResult ExecuteAllowGroup( + PlayerGroup group, + string targetGroupName, + string accessWord + ) + { + TextCommandResult? err = TryResolveGroup(targetGroupName, out PlayerGroup targetGroup); + if (err != null) + return err; + + ClaimLinkModSystem.Registry.GrantGroup(group.Uid, targetGroup.Uid, ParseAccess(accessWord)); + return TextCommandResult.Success( + $"Granted group '{targetGroup.Name}' {accessWord} access to '{group.Name}''s buffer." + ); + } + + private static TextCommandResult ExecuteDenyGroup(PlayerGroup group, string targetGroupName) + { + TextCommandResult? err = TryResolveGroup(targetGroupName, out PlayerGroup targetGroup); + if (err != null) + return err; + + return ClaimLinkModSystem.Registry.RevokeGroup(group.Uid, targetGroup.Uid) + ? TextCommandResult.Success( + $"Revoked group '{targetGroup.Name}''s buffer access to '{group.Name}'." + ) + : TextCommandResult.Error( + $"Group '{targetGroup.Name}' has no buffer access granted in '{group.Name}'." + ); + } + + private static TextCommandResult? ResolveClaimLinkFromWordPair( + TextCommandCallingArgs args, + out PlayerGroup group, + out string targetName + ) + { + string word1 = (string)args[0]; + string? word2 = args.Parsers[1].IsMissing ? null : (string)args[1]; + + if (word2 != null) + { + TextCommandResult? groupErr = TryResolveGroup(word1, out group); + targetName = word2; + return groupErr; + } + + targetName = word1; + int chatGroupId = args.Caller.FromChatGroupId; + if (!ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue(chatGroupId, out group!)) + return TextCommandResult.Error( + "No group specified and you are not sending this from a group chat channel." + ); + + return null; + } + + public static TextCommandResult Allow(TextCommandCallingArgs args) + { + string targetName = (string)args[0]; + string accessWord = (string)args[1]; + + TextCommandResult? err = TryResolveGroupArg(args, 2, out PlayerGroup group); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + err = RequireOpOrOwner(args.Caller.Player, group); + if (err != null) + return err; + + return ExecuteAllow(group, targetName, accessWord); + } + + public static TextCommandResult AllowGroup(TextCommandCallingArgs args) + { + string targetGroupName = (string)args[0]; + string accessWord = (string)args[1]; + + TextCommandResult? err = TryResolveGroupArg(args, 2, out PlayerGroup group); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + err = RequireOpOrOwner(args.Caller.Player, group); + if (err != null) + return err; + + return ExecuteAllowGroup(group, targetGroupName, accessWord); + } + + public static TextCommandResult Deny(TextCommandCallingArgs args) + { + TextCommandResult? err = ResolveClaimLinkFromWordPair(args, out PlayerGroup group, out string targetName); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + err = RequireOpOrOwner(args.Caller.Player, group); + if (err != null) + return err; + + return ExecuteDeny(group, targetName); + } + + public static TextCommandResult DenyGroup(TextCommandCallingArgs args) + { + TextCommandResult? err = ResolveClaimLinkFromWordPair( + args, + out PlayerGroup group, + out string targetGroupName + ); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + err = RequireOpOrOwner(args.Caller.Player, group); + if (err != null) + return err; + + return ExecuteDenyGroup(group, targetGroupName); + } + public static TextCommandResult Delete(TextCommandCallingArgs args) { string playerUid = args.Caller.Player.PlayerUID; @@ -803,9 +1053,45 @@ public static class ClaimLinkChatCommand sb.AppendLine($" {name}: [{string.Join(", ", claims)}]"); } + if (!showDetails) + return sb.ToString(); + + var permittedPlayers = ClaimLinkModSystem.Registry.PermittedPlayersForGroup(groupId); + var permittedGroups = ClaimLinkModSystem.Registry.PermittedGroupsForGroup(groupId); + if (permittedPlayers.Count == 0 && permittedGroups.Count == 0) + return sb.ToString(); + + sb.AppendLine("Buffer access granted:"); + foreach (var (uid, flags) in permittedPlayers) + { + string name = ClaimLinkModSystem.World.PlayerByUid(uid)?.PlayerName ?? uid; + sb.AppendLine($" {name}: {DescribeAccess(flags)}"); + } + foreach (var (targetGroupId, flags) in permittedGroups) + { + string groupLabel = ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue( + targetGroupId, + out PlayerGroup? targetGroup + ) + ? targetGroup!.Name + : $"group {targetGroupId}"; + sb.AppendLine($" {groupLabel} (group): {DescribeAccess(flags)}"); + } + return sb.ToString(); } + private static string DescribeAccess(EnumBlockAccessFlags flags) + { + bool use = (flags & EnumBlockAccessFlags.Use) != 0; + bool buildOrBreak = (flags & EnumBlockAccessFlags.BuildOrBreak) != 0; + if (use && buildOrBreak) + return "all"; + if (buildOrBreak) + return "build"; + return "use"; + } + public static TextCommandResult AdminDelete(TextCommandCallingArgs args) { TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group); @@ -850,6 +1136,68 @@ public static class ClaimLinkChatCommand ); } + public static TextCommandResult AdminAllow(TextCommandCallingArgs args) + { + string targetName = (string)args[0]; + string accessWord = (string)args[1]; + + TextCommandResult? err = TryResolveGroupArg(args, 2, out PlayerGroup group); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + return ExecuteAllow(group, targetName, accessWord); + } + + public static TextCommandResult AdminAllowGroup(TextCommandCallingArgs args) + { + string targetGroupName = (string)args[0]; + string accessWord = (string)args[1]; + + TextCommandResult? err = TryResolveGroupArg(args, 2, out PlayerGroup group); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + return ExecuteAllowGroup(group, targetGroupName, accessWord); + } + + public static TextCommandResult AdminDeny(TextCommandCallingArgs args) + { + TextCommandResult? err = ResolveClaimLinkFromWordPair(args, out PlayerGroup group, out string targetName); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + return ExecuteDeny(group, targetName); + } + + public static TextCommandResult AdminDenyGroup(TextCommandCallingArgs args) + { + TextCommandResult? err = ResolveClaimLinkFromWordPair( + args, + out PlayerGroup group, + out string targetGroupName + ); + if (err != null) + return err; + + err = TryResolveClaimLink(group); + if (err != null) + return err; + + return ExecuteDenyGroup(group, targetGroupName); + } + public static TextCommandResult AdminKick(TextCommandCallingArgs args) { string word1 = (string)args[0]; diff --git a/ClaimLink/ClaimLinkRegistry.cs b/ClaimLink/ClaimLinkRegistry.cs index 3c73296..40a5eeb 100644 --- a/ClaimLink/ClaimLinkRegistry.cs +++ b/ClaimLink/ClaimLinkRegistry.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Vintagestory.API.Common; using Vintagestory.API.Server; namespace ClaimLink; @@ -7,10 +8,17 @@ public class ClaimLinkRegistry { private const string SaveKey = "claimlink:claimlinkdata"; + private static readonly Dictionary EmptyPlayerFlags = new(); + private static readonly Dictionary EmptyGroupFlags = new(); + private readonly ISaveGame saveGame; private readonly List claimLinks = new(); private readonly Dictionary>> entries = new(); + private readonly Dictionary> permittedPlayerUids = + new(); + private readonly Dictionary> permittedPlayerGroupIds = + new(); public ClaimLinkRegistry(ISaveGame saveGame) { @@ -29,6 +37,14 @@ public class ClaimLinkRegistry entries[entry.OwnerPlayerUid][entry.GroupId] = new(entry.ClaimIndicies); } + + foreach (var permission in data.Permissions) + { + if (permission.PermittedPlayerUids.Count > 0) + permittedPlayerUids[permission.GroupId] = new(permission.PermittedPlayerUids); + if (permission.PermittedPlayerGroupIds.Count > 0) + permittedPlayerGroupIds[permission.GroupId] = new(permission.PermittedPlayerGroupIds); + } } public IReadOnlyList All => claimLinks; @@ -46,10 +62,96 @@ public class ClaimLinkRegistry claimLinks.Remove(groupId); foreach (var groups in entries.Values) groups.Remove(groupId); + permittedPlayerUids.Remove(groupId); + permittedPlayerGroupIds.Remove(groupId); Prune(); Save(); } + public void GrantPlayer(int groupId, string uid, EnumBlockAccessFlags flags) + { + if (!permittedPlayerUids.TryGetValue(groupId, out var uids)) + permittedPlayerUids[groupId] = uids = new(); + uids[uid] = flags; + Save(); + } + + public bool RevokePlayer(int groupId, string uid) + { + bool removed = permittedPlayerUids.TryGetValue(groupId, out var uids) && uids.Remove(uid); + if (!removed) + return false; + if (uids!.Count == 0) + permittedPlayerUids.Remove(groupId); + Save(); + return true; + } + + public void GrantGroup(int groupId, int targetGroupId, EnumBlockAccessFlags flags) + { + if (!permittedPlayerGroupIds.TryGetValue(groupId, out var groups)) + permittedPlayerGroupIds[groupId] = groups = new(); + groups[targetGroupId] = flags; + Save(); + } + + public bool RevokeGroup(int groupId, int targetGroupId) + { + bool removed = + permittedPlayerGroupIds.TryGetValue(groupId, out var groups) + && groups.Remove(targetGroupId); + if (!removed) + return false; + if (groups!.Count == 0) + permittedPlayerGroupIds.Remove(groupId); + Save(); + return true; + } + + public bool HasPermission(int groupId, IPlayer player, EnumBlockAccessFlags accessFlag) + { + if ( + permittedPlayerUids.TryGetValue(groupId, out var uids) + && uids.TryGetValue(player.PlayerUID, out EnumBlockAccessFlags flags) + && (flags & accessFlag) != 0 + ) + return true; + + if (permittedPlayerGroupIds.TryGetValue(groupId, out var groups)) + foreach (PlayerGroupMembership membership in player.Groups) + if (groups.TryGetValue(membership.GroupUid, out flags) && (flags & accessFlag) != 0) + return true; + + return false; + } + + public IReadOnlyDictionary PermittedPlayersForGroup(int groupId) => + permittedPlayerUids.TryGetValue(groupId, out var uids) ? uids : EmptyPlayerFlags; + + public IReadOnlyDictionary PermittedGroupsForGroup(int groupId) => + permittedPlayerGroupIds.TryGetValue(groupId, out var groups) ? groups : EmptyGroupFlags; + + public void RemoveGroupFromAllPermissions(int targetGroupId) + { + bool changed = false; + foreach (var groups in permittedPlayerGroupIds.Values) + if (groups.Remove(targetGroupId)) + changed = true; + + if (!changed) + return; + + List? emptyGroupIds = null; + foreach (var (groupId, groups) in permittedPlayerGroupIds) + if (groups.Count == 0) + (emptyGroupIds ??= new()).Add(groupId); + if (emptyGroupIds != null) + foreach (int groupId in emptyGroupIds) + permittedPlayerGroupIds.Remove(groupId); + + Save(); + } + public bool IsClaimLinked(string uid, int claimIndex) => FindGroupContaining(uid, claimIndex) != null; @@ -193,9 +295,35 @@ public class ClaimLinkRegistry } ); + var permissionGroupIds = new HashSet(); + foreach (int groupId in permittedPlayerUids.Keys) + permissionGroupIds.Add(groupId); + foreach (int groupId in permittedPlayerGroupIds.Keys) + permissionGroupIds.Add(groupId); + + var permissionList = new List(); + foreach (int groupId in permissionGroupIds) + { + permittedPlayerUids.TryGetValue(groupId, out var uids); + permittedPlayerGroupIds.TryGetValue(groupId, out var groups); + permissionList.Add( + new ClaimLinkPermissionSaveData + { + GroupId = groupId, + PermittedPlayerUids = uids != null ? new(uids) : new(), + PermittedPlayerGroupIds = groups != null ? new(groups) : new(), + } + ); + } + saveGame.StoreData( SaveKey, - new ClaimLinkSaveData { Links = claimLinks, Entries = entryList } + new ClaimLinkSaveData + { + Links = claimLinks, + Entries = entryList, + Permissions = permissionList, + } ); ClaimLinkBufferIndex.Rebuild(); diff --git a/ClaimLink/CommandListener.cs b/ClaimLink/CommandListener.cs index be9232a..3d904d2 100644 --- a/ClaimLink/CommandListener.cs +++ b/ClaimLink/CommandListener.cs @@ -390,6 +390,8 @@ public class GroupCommandListener : ICommandHookListener if (ClaimLinkModSystem.Registry.Exists(disbandedGroupId)) ClaimLinkModSystem.Registry.Remove(disbandedGroupId); + + ClaimLinkModSystem.Registry.RemoveGroupFromAllPermissions(disbandedGroupId); } } diff --git a/ClaimLink/modinfo.json b/ClaimLink/modinfo.json index 3df6d2c..32d243f 100644 --- a/ClaimLink/modinfo.json +++ b/ClaimLink/modinfo.json @@ -8,7 +8,7 @@ "anth64" ], "description": "Link claims together with groups.", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "game": "1.22.5", "commandhook": "2.1.0"