using System; using System.Collections.Generic; using System.Linq; using System.Text; using Vintagestory.API.Common; using Vintagestory.API.Config; using Vintagestory.API.MathTools; using Vintagestory.API.Server; namespace ClaimLink; public static class ClaimLinkChatCommand { private readonly struct CommandSpec { public readonly string[] Names; public readonly string Description; public readonly ICommandArgumentParser[] Args; public readonly bool IsPlayerOnly; public readonly OnCommandDelegate Handler; public CommandSpec( string[] names, string description, ICommandArgumentParser[] args, bool isPlayerOnly, OnCommandDelegate handler ) { Names = names; Description = description; Args = args; IsPlayerOnly = isPlayerOnly; Handler = handler; } } private static void BuildSubCommand(IChatCommand parent, CommandSpec spec) { var sub = spec.Names.Length > 1 ? parent.BeginSubCommands(spec.Names) : parent.BeginSubCommand(spec.Names[0]); sub.WithDescription(spec.Description); if (spec.Args.Length > 0) sub.WithArgs(spec.Args); if (spec.IsPlayerOnly) sub.RequiresPlayer(); else sub.RequiresPrivilege(Privilege.controlserver); sub.HandleWith(spec.Handler).EndSubCommand(); } public static void Register(ICoreServerAPI api) { var p = api.ChatCommands.Parsers; var root = api .ChatCommands.Create("claimlink") .WithAlias("clink", "claiml", "cl") .WithDescription("Link vanilla land claims together via group.") .RequiresPrivilege(Privilege.chat); CommandSpec[] playerCommands = { new( new[] { "new", "n" }, "Promote a group you own to a claim link.", new ICommandArgumentParser[] { p.OptionalWord("groupname") }, true, New ), new( new[] { "link", "l" }, "Link a claim you own to a claim link.", new ICommandArgumentParser[] { p.IntRange("claim", 0, 999), p.OptionalWord("groupname"), }, true, Link ), new( new[] { "confirm", "c" }, "Confirm pending action.", Array.Empty(), true, Confirm ), new( new[] { "cancel" }, "Cancel pending action.", Array.Empty(), true, Cancel ), new( new[] { "unlink", "ul" }, "Remove a claim you own from its claim link.", new ICommandArgumentParser[] { p.IntRange("claim", 0, 999) }, true, Unlink ), new( new[] { "kick" }, "Force-unlink all of a player's claims from the claim link.", new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") }, 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.", new ICommandArgumentParser[] { p.OptionalWord("groupname") }, true, Delete ), new( new[] { "transferownership", "transfer", "to" }, "Transfer ownership of the claim link to another player.", new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") }, true, TransferOwnership ), new( new[] { "info", "i" }, "Show a claim link's members and linked claims.", new ICommandArgumentParser[] { p.OptionalWord("groupname") }, true, Info ), new( new[] { "list", "ls" }, "List all claim links.", Array.Empty(), true, List ), }; foreach (var spec in playerCommands) BuildSubCommand(root, spec); var admin = root.BeginSubCommands("admin", "a") .WithDescription("Admin commands for claim link.") .RequiresPrivilege(Privilege.controlserver); CommandSpec[] adminCommands = { new( new[] { "delete", "del" }, "Delete a claim link.", new ICommandArgumentParser[] { p.OptionalWord("groupname") }, false, AdminDelete ), new( new[] { "unlink" }, "Unlink a claim.", new ICommandArgumentParser[] { p.Word("playername"), p.IntRange("claim", 0, 999) }, false, AdminUnlink ), new( new[] { "kick" }, "Force-unlink all of a player's claims.", new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") }, false, AdminKick ), new( new[] { "transferownership", "transfer", "to" }, "Transfer ownership of any claim link to another player.", new ICommandArgumentParser[] { p.PlayerUids("playername"), p.OptionalWord("groupname"), }, 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) BuildSubCommand(admin, spec); admin.EndSubCommand(); } internal static readonly Dictionary< string, (int GroupId, Func Action) > PendingActions = new(); internal static readonly Dictionary LastCancelReason = new(); private static TextCommandResult Stage( string playerUid, int groupId, string prompt, Func action ) { PendingActions[playerUid] = (groupId, action); LastCancelReason.Remove(playerUid); return TextCommandResult.Success( $"{prompt} Use /claimlink confirm to proceed, or /claimlink cancel to cancel." ); } internal static void RemovePendingActionsForGroup(int groupId, string reason) { List? toRemove = null; foreach (var (uid, entry) in PendingActions) if (entry.GroupId == groupId) (toRemove ??= new()).Add(uid); if (toRemove == null) return; foreach (string uid in toRemove) { PendingActions.Remove(uid); LastCancelReason[uid] = reason; } } internal static void CancelPendingAction(string uid, int groupId, string reason) { if (!PendingActions.TryGetValue(uid, out var pending) || pending.GroupId != groupId) return; PendingActions.Remove(uid); LastCancelReason[uid] = reason; } private static TextCommandResult? TryResolveGroup(string groupName, out PlayerGroup group) { group = ClaimLinkModSystem.Groups.GetPlayerGroupByName(groupName)!; return group == null ? TextCommandResult.Error($"No group named '{groupName}' exists.") : null; } private static TextCommandResult? TryResolveGroupArg( TextCommandCallingArgs args, int argIndex, out PlayerGroup group ) { if (!args.Parsers[argIndex].IsMissing) return TryResolveGroup((string)args[argIndex], out group); 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; } private static TextCommandResult? TryResolveClaimLink(PlayerGroup group) { return !ClaimLinkModSystem.Registry.Exists(group.Uid) ? TextCommandResult.Error($"'{group.Name}' is not a claim link.") : null; } private static TextCommandResult? RequireMember(IPlayer player, PlayerGroup group) { return player.GetGroup(group.Uid) == null ? TextCommandResult.Error($"'{player.PlayerName}' is not a member of '{group.Name}'.") : null; } private static TextCommandResult? RequireOwner(IPlayer player, PlayerGroup group) { return group.OwnerUID != player.PlayerUID ? TextCommandResult.Error($"You do not own '{group.Name}'.") : null; } private static TextCommandResult? RequireOpOrOwner(IPlayer player, PlayerGroup group) { if (group.OwnerUID == player.PlayerUID) return null; PlayerGroupMembership? membership = player.GetGroup(group.Uid); return membership == null || membership.Level < EnumPlayerGroupMemberShip.Op ? TextCommandResult.Error($"You must be an operator of '{group.Name}' to do that.") : null; } public static TextCommandResult New(TextCommandCallingArgs args) { string playerUid = args.Caller.Player.PlayerUID; TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group); if (err != null) return err; err = RequireOwner(args.Caller.Player, group); if (err != null) return err; if (ClaimLinkModSystem.Registry.Exists(group.Uid)) return TextCommandResult.Error($"'{group.Name}' is already a claim link."); int groupId = group.Uid; string groupName = group.Name; return Stage( playerUid, groupId, $"'{groupName}' will become a claim link.", () => { if (!ClaimLinkModSystem.Groups.PlayerGroupsById.ContainsKey(groupId)) return TextCommandResult.Error($"'{groupName}' no longer exists."); ClaimLinkModSystem.Registry.Add(groupId); return TextCommandResult.Success($"'{groupName}' is now a claim link."); } ); } public static TextCommandResult Confirm(TextCommandCallingArgs args) { string playerUid = args.Caller.Player.PlayerUID; if (!PendingActions.TryGetValue(playerUid, out var pending)) { if (LastCancelReason.Remove(playerUid, out string? reason)) return TextCommandResult.Error($"You do not have a pending action ({reason})."); return TextCommandResult.Error("You do not have a pending action."); } PendingActions.Remove(playerUid); LastCancelReason.Remove(playerUid); return pending.Action(); } public static TextCommandResult Cancel(TextCommandCallingArgs args) { string playerUid = args.Caller.Player.PlayerUID; if (!PendingActions.Remove(playerUid)) return TextCommandResult.Error("You do not have a pending action."); return TextCommandResult.Success("Pending action cancelled."); } public static TextCommandResult Link(TextCommandCallingArgs args) { int claimIndex = (int)args[0]; TextCommandResult? err = TryResolveGroupArg(args, 1, out PlayerGroup group); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; IPlayer player = args.Caller.Player; err = RequireMember(player, group); if (err != null) return err; string playerUid = player.PlayerUID; int groupId = group.Uid; string groupName = group.Name; if ( !ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out LandClaim? claim) || claim == null ) return TextCommandResult.Error($"You don't have a claim at index {claimIndex}."); if (ClaimLinkModSystem.Registry.IsClaimLinked(playerUid, claimIndex)) return TextCommandResult.Error("That claim is already part of a claim link."); List grownAreas = ClaimLinkBuffer.GrowAreas(claim.Areas); ISet excludeGroupIds = new HashSet { groupId }; if ( ClaimLinkBuffer.TryFindOverlappingClaimLink( grownAreas, excludeGroupIds, out int violatingGroupId ) ) { string violatingName = ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue( violatingGroupId, out PlayerGroup? violatingGroup ) ? violatingGroup!.Name : "another claim link"; return TextCommandResult.Error( $"Cannot link that claim, it overlaps the protected buffer of '{violatingName}'." ); } if ( ClaimLinkBuffer.TryFindOverlappingClaim( grownAreas, playerUid, excludeGroupIds, out LandClaim? violatingClaim ) ) { return TextCommandResult.Error( $"Cannot link that claim, its buffer would overlap the claim owned by {violatingClaim!.LastKnownOwnerName}." ); } string claimDesc = DescribeClaim(playerUid, claimIndex); return Stage( playerUid, groupId, $"'{claimDesc}' will be linked into '{groupName}'.", () => { bool stillMember = ClaimLinkModSystem .PlayerData.GetPlayerDataByUid(playerUid) ?.PlayerGroupMemberships.ContainsKey(groupId) == true; if (!stillMember) return TextCommandResult.Error($"You are no longer a member of '{groupName}'."); bool success = ClaimLinkModSystem.Registry.AddEntry(playerUid, groupId, claimIndex); return success ? TextCommandResult.Success($"Linked '{claimDesc}' to '{groupName}'.") : TextCommandResult.Error( $"Unable to link claim {claimIndex} to '{groupName}', it is currently being modified." ); } ); } public static TextCommandResult Unlink(TextCommandCallingArgs args) { int claimIndex = (int)args[0]; string playerUid = args.Caller.Player.PlayerUID; int? groupId = ClaimLinkModSystem.Registry.FindGroupContaining(playerUid, claimIndex); if (groupId == null) return TextCommandResult.Error( $"Claim {claimIndex} is not linked into any claim link by you." ); string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[(int)groupId].Name; string claimDesc = DescribeClaim(playerUid, claimIndex); return Stage( playerUid, (int)groupId, $"'{claimDesc}' will be unlinked from '{groupName}'.", () => { ClaimLinkModSystem.Registry.RemoveEntry(playerUid, (int)groupId, claimIndex); return TextCommandResult.Success($"Unlinked '{claimDesc}' from '{groupName}'."); } ); } public static TextCommandResult Kick(TextCommandCallingArgs args) { string word1 = (string)args[0]; string? word2 = args.Parsers[1].IsMissing ? null : (string)args[1]; string targetName; PlayerGroup group; if (word2 != null) { TextCommandResult? err = TryResolveGroup(word1, out group); if (err != null) return err; targetName = word2; } else { 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." ); targetName = word1; } TextCommandResult? linkErr = TryResolveClaimLink(group); if (linkErr != null) return linkErr; linkErr = RequireOpOrOwner(args.Caller.Player, group); if (linkErr != null) return linkErr; string? targetUid = ClaimLinkModSystem .PlayerData.GetPlayerDataByLastKnownName(targetName) ?.PlayerUID; if (targetUid == null) return TextCommandResult.Error($"No such player '{targetName}'."); string playerUid = args.Caller.Player.PlayerUID; string groupName = group.Name; if (!ClaimLinkModSystem.Registry.HasAnyEntry(targetUid, group.Uid)) return TextCommandResult.Error($"{targetName} has no claims linked in '{groupName}'."); return Stage( playerUid, group.Uid, $"All of {targetName}'s claims will be unlinked from '{groupName}'.", () => { ClaimLinkModSystem.Registry.RemoveAllForPlayerInGroup(targetUid, group.Uid); CommandListener.PendingLoads.Remove(targetUid); PendingActions.Remove(targetUid); return TextCommandResult.Success( $"Unlinked all claims of {targetName} from '{groupName}'." ); } ); } 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; TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; err = RequireOwner(args.Caller.Player, group); if (err != null) return err; int groupId = group.Uid; string groupName = group.Name; return Stage( playerUid, groupId, $"'{groupName}' will be deleted as a claim link.", () => { RemovePendingActionsForGroup(groupId, "claim link was deleted"); ClaimLinkModSystem.Registry.Remove(groupId); return TextCommandResult.Success($"'{groupName}' is no longer a claim link."); } ); } public static TextCommandResult TransferOwnership(TextCommandCallingArgs args) { string word1 = (string)args[0]; string? word2 = args.Parsers[1].IsMissing ? null : (string)args[1]; string targetName; PlayerGroup group; if (word2 != null) { TextCommandResult? groupErr = TryResolveGroup(word1, out group); if (groupErr != null) return groupErr; targetName = word2; } else { 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." ); targetName = word1; } IPlayer player = args.Caller.Player; string playerUid = player.PlayerUID; TextCommandResult? err = TryResolveOnlinePlayerByName(targetName, out IPlayer target); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; err = RequireOwner(player, group); if (err != null) return err; err = RequireMember(target, group); if (err != null) return err; err = RequireNotAlreadyOwner(group, target); if (err != null) return err; string groupName = group.Name; return Stage( playerUid, group.Uid, $"Ownership of '{groupName}' will be transferred to {target.PlayerName}.", () => ExecuteTransferOwnership(group, target) ); } private static TextCommandResult? TryResolveOnlinePlayerByName(string name, out IPlayer target) { target = null!; foreach (IPlayer online in ClaimLinkModSystem.World.AllOnlinePlayers) { if (string.Equals(online.PlayerName, name, StringComparison.OrdinalIgnoreCase)) { target = online; return null; } } return TextCommandResult.Error($"No such player '{name}' online."); } private static TextCommandResult? TryResolveTargetPlayer( PlayerUidName[] matches, out IPlayer target ) { target = null!; if (matches.Length != 1) return TextCommandResult.Error("Name a single player."); IPlayer? resolved = ClaimLinkModSystem.World.PlayerByUid(matches[0].Uid); if (resolved == null) return TextCommandResult.Error($"No such player '{matches[0].Name}'."); target = resolved; return null; } private static TextCommandResult? RequireNotAlreadyOwner(PlayerGroup group, IPlayer target) { return target.PlayerUID == group.OwnerUID ? TextCommandResult.Error($"{target.PlayerName} already owns '{group.Name}'.") : null; } private static TextCommandResult ExecuteTransferOwnership(PlayerGroup group, IPlayer target) { string oldOwnerUid = group.OwnerUID; IPlayer? oldOwner = ClaimLinkModSystem.World.PlayerByUid(oldOwnerUid); PlayerGroupMembership? oldOwnerMembership = oldOwner?.GetGroup(group.Uid); PlayerGroupMembership targetMembership = target.GetGroup(group.Uid)!; group.OwnerUID = target.PlayerUID; targetMembership.Level = EnumPlayerGroupMemberShip.Owner; if (oldOwnerMembership != null) oldOwnerMembership.Level = EnumPlayerGroupMemberShip.Op; PendingActions.Remove(oldOwnerUid); string notice = $"'{group.Name}' is now owned by {target.PlayerName}."; HashSet notifyUids = ClaimLinkModSystem .Registry.MemberUidsForGroup(group.Uid) .ToHashSet(); notifyUids.Add(oldOwnerUid); notifyUids.Add(target.PlayerUID); foreach (string uid in notifyUids) NotifyIfOnline(ClaimLinkModSystem.World.PlayerByUid(uid), notice); return TextCommandResult.Success(notice); } private static void NotifyIfOnline(IPlayer? player, string message) { if (player is IServerPlayer sp && sp.ConnectionState == EnumClientState.Playing) sp.SendMessage(GlobalConstants.GeneralChatGroup, message, EnumChatType.Notification); } public static TextCommandResult Info(TextCommandCallingArgs args) { TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; IPlayer player = args.Caller.Player; bool showDetails = player.HasPrivilege(Privilege.controlserver) || player.GetGroup(group.Uid) != null; return TextCommandResult.Success(FormatInfo(group.Name, group.Uid, showDetails)); } public static TextCommandResult List(TextCommandCallingArgs args) { List groupIds = new(ClaimLinkModSystem.Registry.All); groupIds.Sort((a, b) => GroupMemberCount(b).CompareTo(GroupMemberCount(a))); if (groupIds.Count == 0) return TextCommandResult.Success("There are no claim links."); StringBuilder sb = new(); int shown = 0; foreach (int groupId in groupIds) { if ( !ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue( groupId, out PlayerGroup? group ) ) { RemovePendingActionsForGroup(groupId, "group no longer exists"); ClaimLinkModSystem.Registry.Remove(groupId); continue; } int memberCount = GroupMemberCount(groupId); sb.AppendLine($" {group.Name}: {memberCount} member{(memberCount == 1 ? "" : "s")}"); shown++; } if (shown == 0) return TextCommandResult.Success("There are no claim links."); return TextCommandResult.Success($"Claim links ({shown}):\n{sb}"); } private static IEnumerable GroupMemberUids(int groupId) { foreach (var playerData in ClaimLinkModSystem.PlayerData.PlayerDataByUid.Values) if (playerData.PlayerGroupMemberships.ContainsKey(groupId)) yield return playerData.PlayerUID; } private static int GroupMemberCount(int groupId) { int count = 0; foreach (string _ in GroupMemberUids(groupId)) count++; return count; } private static string DescribeClaim(string ownerPlayerUid, int claimIndex) { if ( !ClaimLinkModSystem.TryResolveOwnedClaim( ownerPlayerUid, claimIndex, out LandClaim? claim ) || claim == null ) return $"claim {claimIndex}"; return string.IsNullOrEmpty(claim.Description) ? $"claim {claimIndex}" : claim.Description; } private static string PlayerNameForUid(string uid) => ClaimLinkModSystem.PlayerData.GetPlayerDataByUid(uid)?.LastKnownPlayername ?? uid; private static string FormatInfo(string groupName, int groupId, bool showDetails) { int memberCount = GroupMemberCount(groupId); StringBuilder sb = new(); sb.AppendLine( $"Claim link '{groupName}' ({memberCount} member{(memberCount == 1 ? "" : "s")}):" ); foreach (string uid in GroupMemberUids(groupId)) { string name = PlayerNameForUid(uid); if (!showDetails) { sb.AppendLine($" {name}"); continue; } IEnumerable claims = ClaimLinkModSystem .Registry.ClaimsForPlayerInGroup(uid, groupId) .Select(i => DescribeClaim(uid, i)); 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 = PlayerNameForUid(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); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; int groupId = group.Uid; string groupName = group.Name; RemovePendingActionsForGroup(groupId, "claim link was deleted"); ClaimLinkModSystem.Registry.Remove(groupId); return TextCommandResult.Success($"'{groupName}' is no longer a claim link."); } public static TextCommandResult AdminUnlink(TextCommandCallingArgs args) { string playerName = (string)args[0]; int claimIndex = (int)args[1]; string? targetUid = ClaimLinkModSystem .PlayerData.GetPlayerDataByLastKnownName(playerName) ?.PlayerUID; if (targetUid == null) return TextCommandResult.Error($"No such player '{playerName}'."); int? groupId = ClaimLinkModSystem.Registry.FindGroupContaining(targetUid, claimIndex); if (groupId == null) return TextCommandResult.Error( $"Claim {claimIndex} is not linked into any claim link by {playerName}." ); string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[(int)groupId].Name; string claimDesc = DescribeClaim(targetUid, claimIndex); ClaimLinkModSystem.Registry.RemoveEntry(targetUid, (int)groupId, claimIndex); return TextCommandResult.Success( $"Unlinked '{claimDesc}' from '{groupName}' (owned by {playerName})." ); } 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]; string? word2 = args.Parsers[1].IsMissing ? null : (string)args[1]; string targetName; PlayerGroup group; if (word2 != null) { TextCommandResult? err = TryResolveGroup(word1, out group); if (err != null) return err; targetName = word2; } else { 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." ); targetName = word1; } TextCommandResult? linkErr = TryResolveClaimLink(group); if (linkErr != null) return linkErr; string? targetUid = ClaimLinkModSystem .PlayerData.GetPlayerDataByLastKnownName(targetName) ?.PlayerUID; if (targetUid == null) return TextCommandResult.Error($"No such player '{targetName}'."); string groupName = group.Name; if (!ClaimLinkModSystem.Registry.HasAnyEntry(targetUid, group.Uid)) return TextCommandResult.Error($"{targetName} has no claims linked in '{groupName}'."); ClaimLinkModSystem.Registry.RemoveAllForPlayerInGroup(targetUid, group.Uid); CommandListener.PendingLoads.Remove(targetUid); PendingActions.Remove(targetUid); return TextCommandResult.Success( $"Unlinked all claims of {targetName} from '{groupName}'." ); } public static TextCommandResult AdminTransferOwnership(TextCommandCallingArgs args) { TextCommandResult? err = TryResolveTargetPlayer( (PlayerUidName[])args[0], out IPlayer target ); if (err != null) return err; err = TryResolveGroupArg(args, 1, out PlayerGroup group); if (err != null) return err; err = TryResolveClaimLink(group); if (err != null) return err; err = RequireMember(target, group); if (err != null) return err; err = RequireNotAlreadyOwner(group, target); if (err != null) return err; return ExecuteTransferOwnership(group, target); } internal static void OnPlayerDisconnect(IServerPlayer player) { PendingActions.Remove(player.PlayerUID); } }