Files
ClaimLink/ClaimLink/ClaimLinkChatCommand.cs
T
2026-07-12 21:19:24 +02:00

389 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Vintagestory.API.Common;
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.Word("groupname") }, true, New),
new(new[] { "link", "l" }, "Link a claim you own to a claim link.",
new ICommandArgumentParser[] { p.Word("groupname"), p.IntRange("claim", 0, 999) }, true, Link),
new(new[] { "confirm", "c" }, "Confirm pending action.",
Array.Empty<ICommandArgumentParser>(), true, Confirm),
new(new[] { "cancel" }, "Cancel pending action.",
Array.Empty<ICommandArgumentParser>(), 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.OnlinePlayer("playername") }, true, Kick),
new(new[] { "delete" }, "Delete a claim link.",
new ICommandArgumentParser[] { p.Word("groupname") }, true, Delete),
new(new[] { "transferownership", "transfer", "to" }, "Transfer ownership of the claim link to another player.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, true, TransferOwnership),
new(new[] { "info", "i" }, "Show a claim link's members and linked claims.",
new ICommandArgumentParser[] { p.Word("groupname") }, true, Info),
new(new[] { "list", "ls" }, "List all claim links.",
Array.Empty<ICommandArgumentParser>(), 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.Word("groupname") }, false, AdminDelete),
new(new[] { "unlink" }, "Unlink a claim.",
new ICommandArgumentParser[] { p.OnlinePlayer("playername"), p.Word("claim") }, false, AdminUnlink),
new(new[] { "kick" }, "Force-unlink all of a player's claims.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, false, AdminKick),
new(new[] { "transferownership", "transfer", "to" }, "Transfer ownership of any claim link to another player.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, false, AdminTransferOwnership),
new(new[] { "info", "i" }, "Show a claim link's members and linked claims.",
new ICommandArgumentParser[] { p.Word("groupname") }, false, AdminInfo),
};
foreach (var spec in adminCommands)
BuildSubCommand(admin, spec);
admin.EndSubCommand();
}
private static readonly Dictionary<string, Func<TextCommandResult>> pendingActions = new();
internal static void RemovePending(string playerUid) => pendingActions.Remove(playerUid);
private static TextCommandResult Stage(string playerUid, string prompt, Func<TextCommandResult> action)
{
pendingActions[playerUid] = action;
return TextCommandResult.Success($"{prompt} Use /claimlink confirm to proceed, or /claimlink cancel to cancel.");
}
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? TryResolveClaimLink(PlayerGroup group, out ClaimLink link)
{
link = ClaimLinkModSystem.Registry.Get(group.Uid)!;
return link == null ? 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($"You are 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)
{
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 groupName = (string)args[0];
string playerUid = args.Caller.Player.PlayerUID;
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) return err;
err = RequireOwner(args.Caller.Player, group);
if (err != null) return err;
if (ClaimLinkModSystem.Registry.Get(group.Uid) != null)
return TextCommandResult.Error($"'{groupName}' is already a claim link.");
int groupId = group.Uid;
return Stage(playerUid, $"'{groupName}' will become a claim link.", () =>
{
ClaimLinkModSystem.Registry.Add(new ClaimLink { GroupId = 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 Func<TextCommandResult>? action))
return TextCommandResult.Error("You do not have a pending action.");
pendingActions.Remove(playerUid);
return 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)
{
string groupName = (string)args[0];
int claimIndex = (int)args[1];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) return err;
err = TryResolveClaimLink(group, out ClaimLink link);
if (err != null) return err;
IPlayer player = args.Caller.Player;
err = RequireMember(player, group);
if (err != null) return err;
string playerUid = player.PlayerUID;
if (!ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out _, out _))
return TextCommandResult.Error("You do not own that claim.");
if (ClaimLinkModSystem.Registry.IsClaimLinked(playerUid, claimIndex))
return TextCommandResult.Error("That claim is already part of a claim link.");
string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage(playerUid, $"{claimDesc} will be linked into '{groupName}'.", () =>
{
ClaimLinkMember? member = link.Members.Find(m => m.OwnerPlayerUid == playerUid);
if (member == null)
{
member = new ClaimLinkMember { OwnerPlayerUid = playerUid };
link.Members.Add(member);
}
member.LocalClaimIndices.Add(claimIndex);
ClaimLinkModSystem.Registry.Save();
return TextCommandResult.Success($"Linked {claimDesc} to '{groupName}'.");
});
}
public static TextCommandResult Unlink(TextCommandCallingArgs args)
{
int claimIndex = (int)args[0];
string playerUid = args.Caller.Player.PlayerUID;
ClaimLink? link = ClaimLinkModSystem.Registry.FindLinkContaining(playerUid, claimIndex);
if (link == null)
return TextCommandResult.Error($"Claim {claimIndex} is not linked into any claim link by you.");
string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[link.GroupId].Name;
string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage(playerUid, $"{claimDesc} will be unlinked from '{groupName}'.", () =>
{
ClaimLinkMember member = link.Members.Find(m => m.OwnerPlayerUid == playerUid)!;
member.LocalClaimIndices.Remove(claimIndex);
if (member.LocalClaimIndices.Count == 0)
link.Members.Remove(member);
ClaimLinkModSystem.Registry.Save();
return TextCommandResult.Success($"Unlinked {claimDesc} from '{groupName}'.");
});
}
public static TextCommandResult Kick(TextCommandCallingArgs args)
{
string groupName = (string)args[0];
IPlayer target = (IPlayer)args[1];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) return err;
err = TryResolveClaimLink(group, out ClaimLink link);
if (err != null) return err;
err = RequireOpOrOwner(args.Caller.Player, group);
if (err != null) return err;
string targetUid = target.PlayerUID;
string playerUid = args.Caller.Player.PlayerUID;
ClaimLinkMember? member = link.Members.Find(m => m.OwnerPlayerUid == targetUid);
if (member == null)
return TextCommandResult.Error($"{target.PlayerName} has no claims linked in '{groupName}'.");
return Stage(playerUid, $"All of {target.PlayerName}'s claims will be unlinked from '{groupName}'.", () =>
{
link.Members.Remove(member);
RemovePending(targetUid);
ClaimLinkModSystem.Registry.Save();
return TextCommandResult.Success($"Unlinked all claims of {target.PlayerName} from '{groupName}'.");
});
}
public static TextCommandResult Delete(TextCommandCallingArgs args)
{
string groupName = (string)args[0];
string playerUid = args.Caller.Player.PlayerUID;
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) return err;
err = TryResolveClaimLink(group, out ClaimLink link);
if (err != null) return err;
err = RequireOwner(args.Caller.Player, group);
if (err != null) return err;
int groupId = group.Uid;
return Stage(playerUid, $"'{groupName}' will be deleted as a claim link.", () =>
{
foreach (ClaimLinkMember member in link.Members)
RemovePending(member.OwnerPlayerUid);
ClaimLinkModSystem.Registry.Remove(groupId);
return TextCommandResult.Success($"'{groupName}' is no longer a claim link.");
});
}
public static TextCommandResult TransferOwnership(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink transferownership");
public static TextCommandResult Info(TextCommandCallingArgs args)
{
string groupName = (string)args[0];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) return err;
err = TryResolveClaimLink(group, out ClaimLink link);
if (err != null) return err;
return TextCommandResult.Success(FormatInfo(groupName, link));
}
public static TextCommandResult List(TextCommandCallingArgs args)
{
List<ClaimLink> links = ClaimLinkModSystem.Registry.All
.OrderByDescending(l => l.Members.Count)
.ToList();
if (links.Count == 0)
return TextCommandResult.Success("There are no claim links.");
StringBuilder sb = new();
sb.AppendLine($"Claim links ({links.Count}):");
foreach (ClaimLink link in links)
{
string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[link.GroupId].Name;
sb.AppendLine($" {groupName}: {link.Members.Count} member{(link.Members.Count == 1 ? "" : "s")}");
}
return TextCommandResult.Success(sb.ToString());
}
private static string DescribeClaim(string ownerPlayerUid, int localIndex)
{
if (!ClaimLinkModSystem.TryResolveOwnedClaim(ownerPlayerUid, localIndex, out _, out LandClaim? claim) || claim == null)
return $"claim {localIndex}";
return string.IsNullOrEmpty(claim.Description) ? $"claim {localIndex}" : claim.Description;
}
private static string FormatInfo(string groupName, ClaimLink link)
{
StringBuilder sb = new();
sb.AppendLine($"Claim link '{groupName}' ({link.Members.Count} member{(link.Members.Count == 1 ? "" : "s")}):");
foreach (ClaimLinkMember member in link.Members)
{
string name = ClaimLinkModSystem.World.PlayerByUid(member.OwnerPlayerUid)?.PlayerName ?? member.OwnerPlayerUid;
IEnumerable<string> claims = member.LocalClaimIndices.Select(i => DescribeClaim(member.OwnerPlayerUid, i));
sb.AppendLine($" {name}: claims [{string.Join(", ", claims)}]");
}
return sb.ToString();
}
public static TextCommandResult AdminDelete(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink admin delete");
public static TextCommandResult AdminUnlink(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink admin unlink");
public static TextCommandResult AdminKick(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink admin kick");
public static TextCommandResult AdminTransferOwnership(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink admin transferownership");
public static TextCommandResult AdminInfo(TextCommandCallingArgs args) => TextCommandResult.Success("stub: claimlink admin info");
}