Compare commits
61
Commits
e435e61a45
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
636a7dee98 | ||
|
|
1e4400555c | ||
|
|
0dfcc5396a | ||
|
|
c110ee2604 | ||
|
|
baf3ccbd93 | ||
|
|
3cf6db1d2a | ||
|
|
12219e6ef5 | ||
|
|
0e0e4e0f74 | ||
|
|
154e7b7848 | ||
|
|
9e37ec11cf | ||
|
|
4d98a115ed | ||
|
|
99238c4744 | ||
|
|
1c079b7e44 | ||
|
|
b7ca9fa53b | ||
|
|
c55787ce40 | ||
|
|
b10d3a0f55 | ||
|
|
b5ceba442a | ||
|
|
d248b772dc | ||
|
|
c8fc86069b | ||
|
|
2e4cc25b20 | ||
|
|
fc7e601f5b | ||
|
|
a2e4a68f9e | ||
|
|
741d283f38 | ||
|
|
9fcf6f1da5 | ||
|
|
f699cd6d54 | ||
|
|
94b6723456 | ||
|
|
de2026d239 | ||
|
|
fffc046126 | ||
|
|
eaa79f8f52 | ||
|
|
1c0a5f88a1 | ||
|
|
d4ef3eba06 | ||
|
|
32dbd4ad91 | ||
|
|
41a2f1a09d | ||
|
|
bee6b6434a | ||
|
|
979f02e7f8 | ||
|
|
acbc45483e | ||
|
|
4e97e25b38 | ||
|
|
1803aaaa37 | ||
|
|
c035bf1a73 | ||
|
|
565f91408b | ||
|
|
e7118e259b | ||
|
|
65e00e2f57 | ||
|
|
c7e3f97a85 | ||
|
|
0914d55d9a | ||
|
|
ac79d2ac94 | ||
|
|
3b6a44a779 | ||
|
|
deafd98f40 | ||
|
|
3b63c5b4af | ||
|
|
cf17697a5e | ||
|
|
02c9a22bf1 | ||
|
|
a470456dcc | ||
|
|
1baa20022b | ||
|
|
5c7fa58ea2 | ||
|
|
9206ecd6a9 | ||
|
|
7d5deb5bdf | ||
|
|
28a89056e6 | ||
|
|
72b59c93ed | ||
|
|
a668e56622 | ||
|
|
ba9d4d992b | ||
|
|
a46c0ea8a8 | ||
|
|
6b72350dfe |
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ProtoBuf;
|
||||||
|
|
||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkSaveData
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public List<int> Links = new();
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public List<ClaimLinkEntrySaveData> Entries = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkEntrySaveData
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string OwnerPlayerUid = "";
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int GroupId;
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public List<int> ClaimIndicies = new();
|
||||||
|
}
|
||||||
@@ -12,6 +12,14 @@
|
|||||||
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
|
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="CommandHook">
|
||||||
|
<HintPath>$(COMMANDHOOK)/CommandHook.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="protobuf-net">
|
||||||
|
<HintPath>$(VINTAGE_STORY)/Lib/protobuf-net.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Vintagestory.API.Common;
|
||||||
|
using Vintagestory.API.MathTools;
|
||||||
|
|
||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
internal static class ClaimLinkBuffer
|
||||||
|
{
|
||||||
|
internal static List<Cuboidi> GetBuffer(int groupId)
|
||||||
|
{
|
||||||
|
List<Cuboidi> cuboids = new();
|
||||||
|
|
||||||
|
foreach (string uid in ClaimLinkModSystem.Registry.MemberUidsForGroup(groupId))
|
||||||
|
{
|
||||||
|
foreach (
|
||||||
|
int claimIndex in ClaimLinkModSystem.Registry.ClaimsForPlayerInGroup(uid, groupId)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!ClaimLinkModSystem.TryResolveOwnedClaim(uid, claimIndex, out LandClaim? claim)
|
||||||
|
|| claim == null
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (Cuboidi area in claim.Areas)
|
||||||
|
cuboids.Add(GrowOne(area));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cuboids;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryFindOverlappingClaimLink(
|
||||||
|
List<Cuboidi> areas,
|
||||||
|
ISet<int>? excludeGroupIds,
|
||||||
|
out int violatingGroupId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
foreach (int groupId in ClaimLinkModSystem.Registry.All)
|
||||||
|
{
|
||||||
|
if (excludeGroupIds != null && excludeGroupIds.Contains(groupId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (Cuboidi bufferCuboid in GetBuffer(groupId))
|
||||||
|
foreach (Cuboidi area in areas)
|
||||||
|
if (area.Intersects(bufferCuboid))
|
||||||
|
{
|
||||||
|
violatingGroupId = groupId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
violatingGroupId = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryFindOverlappingClaim(
|
||||||
|
List<Cuboidi> areas,
|
||||||
|
string excludeOwnerUid,
|
||||||
|
ISet<int>? excludeGroupIds,
|
||||||
|
out LandClaim? violatingClaim
|
||||||
|
)
|
||||||
|
{
|
||||||
|
HashSet<LandClaim> excludedClaims = new();
|
||||||
|
if (excludeGroupIds != null)
|
||||||
|
foreach (int groupId in excludeGroupIds)
|
||||||
|
foreach (string uid in ClaimLinkModSystem.Registry.MemberUidsForGroup(groupId))
|
||||||
|
foreach (
|
||||||
|
int claimIndex in ClaimLinkModSystem.Registry.ClaimsForPlayerInGroup(
|
||||||
|
uid,
|
||||||
|
groupId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
ClaimLinkModSystem.TryResolveOwnedClaim(
|
||||||
|
uid,
|
||||||
|
claimIndex,
|
||||||
|
out LandClaim? excludedClaim
|
||||||
|
)
|
||||||
|
&& excludedClaim != null
|
||||||
|
)
|
||||||
|
excludedClaims.Add(excludedClaim);
|
||||||
|
|
||||||
|
foreach (LandClaim claim in ClaimLinkModSystem.LandClaimAPI.All)
|
||||||
|
{
|
||||||
|
if (claim.OwnedByPlayerUid == excludeOwnerUid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (excludedClaims.Contains(claim))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (Cuboidi claimArea in claim.Areas)
|
||||||
|
foreach (Cuboidi area in areas)
|
||||||
|
if (area.Intersects(claimArea))
|
||||||
|
{
|
||||||
|
violatingClaim = claim;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
violatingClaim = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ISet<int> MemberClaimLinkIds(string uid)
|
||||||
|
{
|
||||||
|
HashSet<int> groupIds = new();
|
||||||
|
var memberships = ClaimLinkModSystem.PlayerData.GetPlayerDataByUid(uid)?.PlayerGroupMemberships;
|
||||||
|
if (memberships == null)
|
||||||
|
return groupIds;
|
||||||
|
|
||||||
|
foreach (int groupId in memberships.Keys)
|
||||||
|
if (ClaimLinkModSystem.Registry.Exists(groupId))
|
||||||
|
groupIds.Add(groupId);
|
||||||
|
|
||||||
|
return groupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<Cuboidi> GrowAreas(List<Cuboidi> areas)
|
||||||
|
{
|
||||||
|
List<Cuboidi> grown = new();
|
||||||
|
foreach (Cuboidi area in areas)
|
||||||
|
grown.Add(GrowOne(area));
|
||||||
|
return grown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Cuboidi GrowOne(Cuboidi area)
|
||||||
|
{
|
||||||
|
Cuboidi normalized = new Cuboidi(
|
||||||
|
area.MinX,
|
||||||
|
area.MinY,
|
||||||
|
area.MinZ,
|
||||||
|
area.MaxX,
|
||||||
|
area.MaxY,
|
||||||
|
area.MaxZ
|
||||||
|
);
|
||||||
|
int bufferSize = ClaimLinkModSystem.Config.BufferSize;
|
||||||
|
return normalized.GrowBy(bufferSize, bufferSize, bufferSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,933 @@
|
|||||||
|
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<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.OptionalWord("playername") },
|
||||||
|
true,
|
||||||
|
Kick
|
||||||
|
),
|
||||||
|
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<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.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
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var spec in adminCommands)
|
||||||
|
BuildSubCommand(admin, spec);
|
||||||
|
|
||||||
|
admin.EndSubCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static readonly Dictionary<
|
||||||
|
string,
|
||||||
|
(int GroupId, Func<TextCommandResult> Action)
|
||||||
|
> PendingActions = new();
|
||||||
|
|
||||||
|
internal static readonly Dictionary<string, string> LastCancelReason = new();
|
||||||
|
|
||||||
|
private static TextCommandResult Stage(
|
||||||
|
string playerUid,
|
||||||
|
int groupId,
|
||||||
|
string prompt,
|
||||||
|
Func<TextCommandResult> 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<string>? 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<Cuboidi> grownAreas = ClaimLinkBuffer.GrowAreas(claim.Areas);
|
||||||
|
ISet<int> excludeGroupIds = new HashSet<int> { 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}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> 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<int> 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<string> 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 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 = ClaimLinkModSystem.World.PlayerByUid(uid)?.PlayerName ?? uid;
|
||||||
|
|
||||||
|
if (!showDetails)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<string> claims = ClaimLinkModSystem
|
||||||
|
.Registry.ClaimsForPlayerInGroup(uid, groupId)
|
||||||
|
.Select(i => DescribeClaim(uid, i));
|
||||||
|
sb.AppendLine($" {name}: [{string.Join(", ", claims)}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
internal class ClaimLinkConfig
|
||||||
|
{
|
||||||
|
public int BufferSize = 16;
|
||||||
|
}
|
||||||
@@ -1,7 +1,105 @@
|
|||||||
using Vintagestory.API.Common;
|
using System.Collections.Generic;
|
||||||
|
using CommandHook;
|
||||||
|
using Vintagestory.API.Common;
|
||||||
|
using Vintagestory.API.Server;
|
||||||
|
|
||||||
namespace ClaimLink;
|
namespace ClaimLink;
|
||||||
|
|
||||||
public class ClaimLinkModSystem : ModSystem
|
public class ClaimLinkModSystem : ModSystem
|
||||||
{
|
{
|
||||||
|
internal static ILandClaimAPI LandClaimAPI = null!;
|
||||||
|
internal static ILogger Logger = null!;
|
||||||
|
internal static ClaimLinkRegistry Registry = null!;
|
||||||
|
internal static IGroupManager Groups = null!;
|
||||||
|
internal static IWorldAccessor World = null!;
|
||||||
|
internal static IPlayerDataManager PlayerData = null!;
|
||||||
|
internal static ClaimLinkConfig Config = null!;
|
||||||
|
internal CommandListener? CmdListener;
|
||||||
|
internal GroupCommandListener? GroupCmdListener;
|
||||||
|
|
||||||
|
public override bool ShouldLoad(EnumAppSide forSide) => forSide == EnumAppSide.Server;
|
||||||
|
|
||||||
|
public override void StartServerSide(ICoreServerAPI api)
|
||||||
|
{
|
||||||
|
LandClaimAPI = api.World.Claims;
|
||||||
|
World = api.World;
|
||||||
|
Groups = api.Groups;
|
||||||
|
Logger = api.Logger;
|
||||||
|
PlayerData = api.PlayerData;
|
||||||
|
Config = api.LoadModConfig<ClaimLinkConfig>("claimlink.json") ?? new ClaimLinkConfig();
|
||||||
|
api.StoreModConfig(Config, "claimlink.json");
|
||||||
|
Registry = new ClaimLinkRegistry(api.WorldManager.SaveGame);
|
||||||
|
|
||||||
|
CmdListener = new CommandListener();
|
||||||
|
CommandHookModSystem.Register(CmdListener);
|
||||||
|
GroupCmdListener = new GroupCommandListener();
|
||||||
|
CommandHookModSystem.Register(GroupCmdListener);
|
||||||
|
ClaimLinkChatCommand.Register(api);
|
||||||
|
ClaimLinkVisualizerNetwork.Start(api);
|
||||||
|
|
||||||
|
api.Event.PlayerDisconnect += CommandListener.OnPlayerDisconnect;
|
||||||
|
api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect;
|
||||||
|
api.Event.PlayerDisconnect += ClaimLinkChatCommand.OnPlayerDisconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
if (CmdListener != null)
|
||||||
|
CommandHookModSystem.Unregister(CmdListener);
|
||||||
|
if (GroupCmdListener != null)
|
||||||
|
CommandHookModSystem.Unregister(GroupCmdListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(int claimIndex, LandClaim claim)> EnumerateOwnedClaims(
|
||||||
|
string ownerPlayerUid
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (LandClaim c in LandClaimAPI.All)
|
||||||
|
{
|
||||||
|
if (c.OwnedByPlayerUid != ownerPlayerUid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
yield return (index, c);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryResolveOwnedClaim(
|
||||||
|
string ownerPlayerUid,
|
||||||
|
int claimIndex,
|
||||||
|
out LandClaim? claim
|
||||||
|
)
|
||||||
|
{
|
||||||
|
foreach ((int index, LandClaim c) in EnumerateOwnedClaims(ownerPlayerUid))
|
||||||
|
{
|
||||||
|
if (index == claimIndex)
|
||||||
|
{
|
||||||
|
claim = c;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
claim = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryResolveClaimIndex(
|
||||||
|
string ownerPlayerUid,
|
||||||
|
LandClaim target,
|
||||||
|
out int claimIndex
|
||||||
|
)
|
||||||
|
{
|
||||||
|
foreach ((int index, LandClaim c) in EnumerateOwnedClaims(ownerPlayerUid))
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(c, target))
|
||||||
|
{
|
||||||
|
claimIndex = index;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
claimIndex = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Vintagestory.API.Server;
|
||||||
|
|
||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
public class ClaimLinkRegistry
|
||||||
|
{
|
||||||
|
private const string SaveKey = "claimlink:claimlinkdata";
|
||||||
|
|
||||||
|
private readonly ISaveGame saveGame;
|
||||||
|
|
||||||
|
private readonly List<int> claimLinks = new();
|
||||||
|
private readonly Dictionary<string, Dictionary<int, List<int>>> entries = new();
|
||||||
|
|
||||||
|
public ClaimLinkRegistry(ISaveGame saveGame)
|
||||||
|
{
|
||||||
|
this.saveGame = saveGame;
|
||||||
|
|
||||||
|
ClaimLinkSaveData? data = saveGame.GetData<ClaimLinkSaveData?>(SaveKey, null);
|
||||||
|
if (data == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
claimLinks.AddRange(data.Links);
|
||||||
|
|
||||||
|
foreach (var entry in data.Entries)
|
||||||
|
{
|
||||||
|
if (!entries.ContainsKey(entry.OwnerPlayerUid))
|
||||||
|
entries[entry.OwnerPlayerUid] = new Dictionary<int, List<int>>();
|
||||||
|
|
||||||
|
entries[entry.OwnerPlayerUid][entry.GroupId] = new(entry.ClaimIndicies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<int> All => claimLinks;
|
||||||
|
|
||||||
|
public bool Exists(int groupId) => claimLinks.Contains(groupId);
|
||||||
|
|
||||||
|
public void Add(int groupId)
|
||||||
|
{
|
||||||
|
claimLinks.Add(groupId);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int groupId)
|
||||||
|
{
|
||||||
|
claimLinks.Remove(groupId);
|
||||||
|
foreach (var groups in entries.Values)
|
||||||
|
groups.Remove(groupId);
|
||||||
|
Prune();
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsClaimLinked(string uid, int claimIndex) =>
|
||||||
|
FindGroupContaining(uid, claimIndex) != null;
|
||||||
|
|
||||||
|
public int? FindGroupContaining(string uid, int claimIndex)
|
||||||
|
{
|
||||||
|
if (!entries.TryGetValue(uid, out var groups))
|
||||||
|
return null;
|
||||||
|
foreach (var (groupId, claims) in groups)
|
||||||
|
if (claims.Contains(claimIndex))
|
||||||
|
return groupId;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddEntry(string uid, int groupId, int claimIndex)
|
||||||
|
{
|
||||||
|
if (CommandListener.PendingLoads.ContainsKey(uid))
|
||||||
|
return false;
|
||||||
|
if (!Exists(groupId))
|
||||||
|
return false;
|
||||||
|
if (!entries.TryGetValue(uid, out var groups))
|
||||||
|
entries[uid] = groups = new();
|
||||||
|
if (!groups.TryGetValue(groupId, out var claims))
|
||||||
|
groups[groupId] = claims = new();
|
||||||
|
claims.Add(claimIndex);
|
||||||
|
Save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveEntry(string uid, int groupId, int claimIndex)
|
||||||
|
{
|
||||||
|
if (entries.TryGetValue(uid, out var groups) && groups.TryGetValue(groupId, out var claims))
|
||||||
|
claims.Remove(claimIndex);
|
||||||
|
Prune();
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClaimRemoved(string uid, int removedIndex)
|
||||||
|
{
|
||||||
|
if (!entries.TryGetValue(uid, out var groups))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var claims in groups.Values)
|
||||||
|
{
|
||||||
|
claims.Remove(removedIndex);
|
||||||
|
for (int i = 0; i < claims.Count; i++)
|
||||||
|
if (claims[i] > removedIndex)
|
||||||
|
claims[i]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Prune();
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClaimSaved(string uid, int claimIndex)
|
||||||
|
{
|
||||||
|
if (!entries.TryGetValue(uid, out var groups))
|
||||||
|
return;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (Vintagestory.API.Common.LandClaim c in ClaimLinkModSystem.LandClaimAPI.All)
|
||||||
|
if (c.OwnedByPlayerUid == uid)
|
||||||
|
count++;
|
||||||
|
int newIndex = count - 1;
|
||||||
|
|
||||||
|
foreach (var claims in groups.Values)
|
||||||
|
{
|
||||||
|
int i = claims.IndexOf(claimIndex);
|
||||||
|
|
||||||
|
for (int j = 0; j < claims.Count; j++)
|
||||||
|
if (j != i && claims[j] > claimIndex)
|
||||||
|
claims[j]--;
|
||||||
|
|
||||||
|
if (i >= 0)
|
||||||
|
claims[i] = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAnyEntry(string uid, int groupId) =>
|
||||||
|
entries.TryGetValue(uid, out var groups)
|
||||||
|
&& groups.TryGetValue(groupId, out var claims)
|
||||||
|
&& claims.Count > 0;
|
||||||
|
|
||||||
|
public void RemoveAllForPlayerInGroup(string uid, int groupId)
|
||||||
|
{
|
||||||
|
if (entries.TryGetValue(uid, out var groups))
|
||||||
|
groups.Remove(groupId);
|
||||||
|
Prune();
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllForPlayer(string uid)
|
||||||
|
{
|
||||||
|
entries.Remove(uid);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> MemberUidsForGroup(int groupId)
|
||||||
|
{
|
||||||
|
foreach (var (uid, groups) in entries)
|
||||||
|
if (groups.ContainsKey(groupId))
|
||||||
|
yield return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MemberCountForGroup(int groupId)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (var uid in MemberUidsForGroup(groupId))
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<int> ClaimsForPlayerInGroup(string uid, int groupId) =>
|
||||||
|
entries.TryGetValue(uid, out var groups) && groups.TryGetValue(groupId, out var claims)
|
||||||
|
? claims
|
||||||
|
: System.Array.Empty<int>();
|
||||||
|
|
||||||
|
private void Prune()
|
||||||
|
{
|
||||||
|
List<string>? empty = null;
|
||||||
|
foreach (var (uid, groups) in entries)
|
||||||
|
if (groups.Count == 0)
|
||||||
|
(empty ??= new()).Add(uid);
|
||||||
|
if (empty != null)
|
||||||
|
foreach (var uid in empty)
|
||||||
|
entries.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
var entryList = new List<ClaimLinkEntrySaveData>();
|
||||||
|
foreach (var (uid, groups) in entries)
|
||||||
|
foreach (var (groupId, claims) in groups)
|
||||||
|
entryList.Add(
|
||||||
|
new ClaimLinkEntrySaveData
|
||||||
|
{
|
||||||
|
OwnerPlayerUid = uid,
|
||||||
|
GroupId = groupId,
|
||||||
|
ClaimIndicies = new(claims),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
saveGame.StoreData(
|
||||||
|
SaveKey,
|
||||||
|
new ClaimLinkSaveData { Links = claimLinks, Entries = entryList }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ProtoBuf;
|
||||||
|
using Vintagestory.API.MathTools;
|
||||||
|
using Vintagestory.API.Server;
|
||||||
|
|
||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkBufferRequest { }
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkBufferCuboidDto
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int MinX;
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int MinY;
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int MinZ;
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int MaxX;
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public int MaxY;
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public int MaxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkBufferEntryDto
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int GroupId;
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public List<ClaimLinkBufferCuboidDto> Cuboids = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ClaimLinkBufferResponse
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public List<ClaimLinkBufferEntryDto> Entries = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ClaimLinkVisualizerNetwork
|
||||||
|
{
|
||||||
|
internal const string ChannelName = "claimlinkvisualizer";
|
||||||
|
|
||||||
|
internal static void Start(ICoreServerAPI api)
|
||||||
|
{
|
||||||
|
api.Network
|
||||||
|
.RegisterChannel(ChannelName)
|
||||||
|
.RegisterMessageType<ClaimLinkBufferRequest>()
|
||||||
|
.RegisterMessageType<ClaimLinkBufferResponse>()
|
||||||
|
.SetMessageHandler<ClaimLinkBufferRequest>((fromPlayer, _) =>
|
||||||
|
SendBuffers(api, fromPlayer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendBuffers(ICoreServerAPI api, IServerPlayer toPlayer)
|
||||||
|
{
|
||||||
|
ClaimLinkBufferResponse response = new();
|
||||||
|
|
||||||
|
BlockPos playerPos = toPlayer.Entity.Pos.AsBlockPos;
|
||||||
|
int radius = toPlayer.WorldData.LastApprovedViewDistance;
|
||||||
|
|
||||||
|
foreach (int groupId in ClaimLinkModSystem.Registry.All)
|
||||||
|
{
|
||||||
|
ClaimLinkBufferEntryDto entry = new() { GroupId = groupId };
|
||||||
|
foreach (Cuboidi cuboid in ClaimLinkBuffer.GetBuffer(groupId))
|
||||||
|
{
|
||||||
|
if (!IsNearby(cuboid, playerPos, radius))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
entry.Cuboids.Add(
|
||||||
|
new ClaimLinkBufferCuboidDto
|
||||||
|
{
|
||||||
|
MinX = cuboid.MinX,
|
||||||
|
MinY = cuboid.MinY,
|
||||||
|
MinZ = cuboid.MinZ,
|
||||||
|
MaxX = cuboid.MaxX,
|
||||||
|
MaxY = cuboid.MaxY,
|
||||||
|
MaxZ = cuboid.MaxZ,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Cuboids.Count > 0)
|
||||||
|
response.Entries.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Network.GetChannel(ChannelName).SendPacket(response, toPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNearby(Cuboidi cuboid, BlockPos playerPos, int radius) =>
|
||||||
|
cuboid.MinX <= playerPos.X + radius
|
||||||
|
&& cuboid.MaxX >= playerPos.X - radius
|
||||||
|
&& cuboid.MinY <= playerPos.Y + radius
|
||||||
|
&& cuboid.MaxY >= playerPos.Y - radius
|
||||||
|
&& cuboid.MinZ <= playerPos.Z + radius
|
||||||
|
&& cuboid.MaxZ >= playerPos.Z - radius;
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CommandHook;
|
||||||
|
using Vintagestory.API.Common;
|
||||||
|
using Vintagestory.API.Config;
|
||||||
|
using Vintagestory.API.MathTools;
|
||||||
|
using Vintagestory.API.Server;
|
||||||
|
|
||||||
|
namespace ClaimLink;
|
||||||
|
|
||||||
|
public class CommandListener : ICommandHookListener
|
||||||
|
{
|
||||||
|
public string ModId => "claimlink";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Commands => new[] { "land" };
|
||||||
|
|
||||||
|
public CommandRegistration Registration => new(Before, After);
|
||||||
|
|
||||||
|
internal static Dictionary<string, int> PendingLoads = new Dictionary<string, int>();
|
||||||
|
internal static Dictionary<string, List<Cuboidi>?> PendingSaveChecks =
|
||||||
|
new Dictionary<string, List<Cuboidi>?>();
|
||||||
|
|
||||||
|
private delegate void SubHandler(Caller caller, CmdArgs args);
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, SubHandler> topLevel = new()
|
||||||
|
{
|
||||||
|
["claim"] = Claim,
|
||||||
|
["free"] = Free,
|
||||||
|
["adminfree"] = AdminFree,
|
||||||
|
["adminfreehere"] = AdminFreeHere,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, SubHandler> claimSub = new()
|
||||||
|
{
|
||||||
|
["load"] = ClaimLoad,
|
||||||
|
["new"] = ClaimNew,
|
||||||
|
["save"] = ClaimSave,
|
||||||
|
["cancel"] = ClaimCancel,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void Dispatch(TextCommandCallingArgs args)
|
||||||
|
{
|
||||||
|
CmdArgs rawArgs = args.RawArgs.Clone();
|
||||||
|
|
||||||
|
while (rawArgs.Length > 0)
|
||||||
|
{
|
||||||
|
string word = rawArgs.PeekWord();
|
||||||
|
|
||||||
|
if (topLevel.TryGetValue(word, out SubHandler? handler))
|
||||||
|
{
|
||||||
|
rawArgs.PopWord();
|
||||||
|
handler(args.Caller, rawArgs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rawArgs.PopWord();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextCommandResult? Before(TextCommandCallingArgs args)
|
||||||
|
{
|
||||||
|
Dispatch(args);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void After(TextCommandCallingArgs args, TextCommandResult result)
|
||||||
|
{
|
||||||
|
if (args.Caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string uid = args.Caller.Player.PlayerUID;
|
||||||
|
if (!PendingSaveChecks.Remove(uid, out List<Cuboidi>? snapshot))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (result.Status != EnumCommandStatus.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<LandClaim> allClaims = ClaimLinkModSystem.LandClaimAPI.All;
|
||||||
|
if (allClaims.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LandClaim savedClaim = allClaims[allClaims.Count - 1];
|
||||||
|
if (savedClaim.OwnedByPlayerUid != uid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int? linkedGroupId = null;
|
||||||
|
if (ClaimLinkModSystem.TryResolveClaimIndex(uid, savedClaim, out int savedIndex))
|
||||||
|
linkedGroupId = ClaimLinkModSystem.Registry.FindGroupContaining(uid, savedIndex);
|
||||||
|
|
||||||
|
bool isLinked = linkedGroupId != null;
|
||||||
|
List<Cuboidi> checkAreas = isLinked
|
||||||
|
? ClaimLinkBuffer.GrowAreas(savedClaim.Areas)
|
||||||
|
: savedClaim.Areas;
|
||||||
|
|
||||||
|
ISet<int> excludeGroupIds = isLinked
|
||||||
|
? new HashSet<int> { linkedGroupId!.Value }
|
||||||
|
: ClaimLinkBuffer.MemberClaimLinkIds(uid);
|
||||||
|
|
||||||
|
LandClaim? violatingClaim = null;
|
||||||
|
bool violatesClaimLink = ClaimLinkBuffer.TryFindOverlappingClaimLink(
|
||||||
|
checkAreas,
|
||||||
|
excludeGroupIds,
|
||||||
|
out int violatingGroupId
|
||||||
|
);
|
||||||
|
bool violatesClaim =
|
||||||
|
isLinked
|
||||||
|
&& !violatesClaimLink
|
||||||
|
&& ClaimLinkBuffer.TryFindOverlappingClaim(
|
||||||
|
checkAreas,
|
||||||
|
uid,
|
||||||
|
excludeGroupIds,
|
||||||
|
out violatingClaim
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!violatesClaimLink && !violatesClaim)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClaimLinkModSystem.LandClaimAPI.Remove(savedClaim);
|
||||||
|
|
||||||
|
string reason = violatesClaimLink
|
||||||
|
? (
|
||||||
|
ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue(
|
||||||
|
violatingGroupId,
|
||||||
|
out PlayerGroup? violatingGroup
|
||||||
|
)
|
||||||
|
? $"it overlaps the protected buffer of '{violatingGroup!.Name}'"
|
||||||
|
: "it overlaps the protected buffer of another claim link"
|
||||||
|
)
|
||||||
|
: $"its buffer would overlap the claim owned by {violatingClaim!.LastKnownOwnerName}";
|
||||||
|
|
||||||
|
if (snapshot != null)
|
||||||
|
{
|
||||||
|
savedClaim.Areas.Clear();
|
||||||
|
savedClaim.Areas.AddRange(snapshot);
|
||||||
|
ClaimLinkModSystem.LandClaimAPI.Add(savedClaim);
|
||||||
|
NotifyPlayer(args.Caller.Player, $"Your claim growth was reverted: {reason}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NotifyPlayer(args.Caller.Player, $"Your claim was rejected: {reason}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NotifyPlayer(IPlayer player, string message)
|
||||||
|
{
|
||||||
|
if (player is IServerPlayer sp)
|
||||||
|
sp.SendMessage(GlobalConstants.GeneralChatGroup, message, EnumChatType.Notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Claim(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
string? word = args.PeekWord();
|
||||||
|
if (word == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (claimSub.TryGetValue(word, out SubHandler? handler))
|
||||||
|
{
|
||||||
|
args.PopWord();
|
||||||
|
handler(caller, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClaimSave(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string uid = caller.Player.PlayerUID;
|
||||||
|
PendingSaveChecks[uid] = null;
|
||||||
|
|
||||||
|
if (!PendingLoads.TryGetValue(uid, out int pendingIndex))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
ClaimLinkModSystem.TryResolveOwnedClaim(uid, pendingIndex, out LandClaim? existingClaim)
|
||||||
|
&& existingClaim != null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
List<Cuboidi> snapshot = new();
|
||||||
|
foreach (Cuboidi area in existingClaim.Areas)
|
||||||
|
snapshot.Add(area.Clone());
|
||||||
|
PendingSaveChecks[uid] = snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClaimLinkModSystem.Registry.ClaimSaved(uid, pendingIndex);
|
||||||
|
PendingLoads.Remove(uid);
|
||||||
|
ClaimLinkChatCommand.PendingActions.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClaimLoad(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
int? claimIndex = args.PopInt();
|
||||||
|
if (caller.Player == null || claimIndex == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
ClaimLinkModSystem.TryResolveOwnedClaim(caller.Player.PlayerUID, (int)claimIndex, out _)
|
||||||
|
)
|
||||||
|
PendingLoads[caller.Player.PlayerUID] = (int)claimIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClaimNew(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PendingLoads.Remove(caller.Player.PlayerUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClaimCancel(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PendingLoads.Remove(caller.Player.PlayerUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Free(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string uid = caller.Player.PlayerUID;
|
||||||
|
int? claimIndex = args.PopInt();
|
||||||
|
if (claimIndex == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.PopWord() != "confirm")
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClaimLinkModSystem.Registry.ClaimRemoved(uid, (int)claimIndex);
|
||||||
|
ClaimLinkChatCommand.PendingActions.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AdminFree(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? name = args.PopWord();
|
||||||
|
if (name == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? uid = ClaimLinkModSystem.PlayerData.GetPlayerDataByLastKnownName(name)?.PlayerUID;
|
||||||
|
if (uid == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClaimLinkModSystem.Registry.ClearAllForPlayer(uid);
|
||||||
|
ClaimLinkChatCommand.PendingActions.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AdminFreeHere(Caller caller, CmdArgs args)
|
||||||
|
{
|
||||||
|
if (caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LandClaim[] claims = ClaimLinkModSystem.LandClaimAPI.Get(
|
||||||
|
caller.Player.Entity.Pos.AsBlockPos
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (LandClaim c in claims)
|
||||||
|
if (ClaimLinkModSystem.TryResolveClaimIndex(c.OwnedByPlayerUid, c, out int claimIndex))
|
||||||
|
{
|
||||||
|
ClaimLinkModSystem.Registry.ClaimRemoved(c.OwnedByPlayerUid, claimIndex);
|
||||||
|
ClaimLinkChatCommand.PendingActions.Remove(c.OwnedByPlayerUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void OnPlayerDisconnect(IServerPlayer player)
|
||||||
|
{
|
||||||
|
PendingLoads.Remove(player.PlayerUID);
|
||||||
|
PendingSaveChecks.Remove(player.PlayerUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GroupCommandListener : ICommandHookListener
|
||||||
|
{
|
||||||
|
public string ModId => "claimlink.group";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Commands => new[] { "group" };
|
||||||
|
|
||||||
|
public CommandRegistration Registration => new(Before, After);
|
||||||
|
|
||||||
|
internal static Dictionary<string, int> PendingDisbands = new Dictionary<string, int>();
|
||||||
|
internal static Dictionary<string, (string AffectedUid, int GroupId)> PendingMembershipChecks =
|
||||||
|
new Dictionary<string, (string, int)>();
|
||||||
|
|
||||||
|
private TextCommandResult? Before(TextCommandCallingArgs args)
|
||||||
|
{
|
||||||
|
if (args.Caller.Player == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string callerUid = args.Caller.Player.PlayerUID;
|
||||||
|
CmdArgs rawArgs = args.RawArgs.Clone();
|
||||||
|
string? word = rawArgs.PopWord();
|
||||||
|
|
||||||
|
switch (word)
|
||||||
|
{
|
||||||
|
case "confirmdisband":
|
||||||
|
ConfirmDisband(args, callerUid, rawArgs);
|
||||||
|
break;
|
||||||
|
case "leave":
|
||||||
|
Leave(args, callerUid, rawArgs);
|
||||||
|
break;
|
||||||
|
case "kick":
|
||||||
|
Kick(args, callerUid, rawArgs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfirmDisband(
|
||||||
|
TextCommandCallingArgs args,
|
||||||
|
string callerUid,
|
||||||
|
CmdArgs rawArgs
|
||||||
|
)
|
||||||
|
{
|
||||||
|
string? name = rawArgs.PopWord();
|
||||||
|
int? groupId =
|
||||||
|
name != null
|
||||||
|
? ClaimLinkModSystem.Groups.GetPlayerGroupByName(name)?.Uid
|
||||||
|
: args.Caller.FromChatGroupId;
|
||||||
|
|
||||||
|
if (groupId != null)
|
||||||
|
PendingDisbands[callerUid] = (int)groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Leave(TextCommandCallingArgs args, string callerUid, CmdArgs rawArgs)
|
||||||
|
{
|
||||||
|
string? name = rawArgs.PopWord();
|
||||||
|
int? groupId =
|
||||||
|
name != null
|
||||||
|
? ClaimLinkModSystem.Groups.GetPlayerGroupByName(name)?.Uid
|
||||||
|
: args.Caller.FromChatGroupId;
|
||||||
|
|
||||||
|
if (groupId != null)
|
||||||
|
PendingMembershipChecks[callerUid] = (callerUid, (int)groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Kick(TextCommandCallingArgs args, string callerUid, CmdArgs rawArgs)
|
||||||
|
{
|
||||||
|
string? word1 = rawArgs.PopWord();
|
||||||
|
string? word2 = rawArgs.PopWord();
|
||||||
|
|
||||||
|
string? targetName;
|
||||||
|
int? groupId;
|
||||||
|
if (word2 != null)
|
||||||
|
{
|
||||||
|
groupId =
|
||||||
|
word1 != null ? ClaimLinkModSystem.Groups.GetPlayerGroupByName(word1)?.Uid : null;
|
||||||
|
targetName = word2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetName = word1;
|
||||||
|
groupId = args.Caller.FromChatGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetName == null || groupId == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? targetUid = ClaimLinkModSystem
|
||||||
|
.PlayerData.GetPlayerDataByLastKnownName(targetName)
|
||||||
|
?.PlayerUID;
|
||||||
|
if (targetUid != null)
|
||||||
|
PendingMembershipChecks[callerUid] = (targetUid, (int)groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void After(TextCommandCallingArgs args, TextCommandResult result)
|
||||||
|
{
|
||||||
|
if (args.Caller.Player == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string callerUid = args.Caller.Player.PlayerUID;
|
||||||
|
|
||||||
|
if (PendingDisbands.TryGetValue(callerUid, out int disbandedGroupId))
|
||||||
|
{
|
||||||
|
PendingDisbands.Remove(callerUid);
|
||||||
|
|
||||||
|
if (!ClaimLinkModSystem.Groups.PlayerGroupsById.ContainsKey(disbandedGroupId))
|
||||||
|
{
|
||||||
|
ClaimLinkChatCommand.RemovePendingActionsForGroup(
|
||||||
|
disbandedGroupId,
|
||||||
|
"group no longer exists"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ClaimLinkModSystem.Registry.Exists(disbandedGroupId))
|
||||||
|
ClaimLinkModSystem.Registry.Remove(disbandedGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PendingMembershipChecks.TryGetValue(callerUid, out var check))
|
||||||
|
{
|
||||||
|
PendingMembershipChecks.Remove(callerUid);
|
||||||
|
|
||||||
|
bool stillMember =
|
||||||
|
ClaimLinkModSystem
|
||||||
|
.PlayerData.GetPlayerDataByUid(check.AffectedUid)
|
||||||
|
?.PlayerGroupMemberships.ContainsKey(check.GroupId) == true;
|
||||||
|
|
||||||
|
if (!stillMember)
|
||||||
|
ClaimLinkChatCommand.CancelPendingAction(
|
||||||
|
check.AffectedUid,
|
||||||
|
check.GroupId,
|
||||||
|
"you are no longer a member of the group"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void OnPlayerDisconnect(IServerPlayer player)
|
||||||
|
{
|
||||||
|
PendingDisbands.Remove(player.PlayerUID);
|
||||||
|
PendingMembershipChecks.Remove(player.PlayerUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@
|
|||||||
"type": "Code",
|
"type": "Code",
|
||||||
"modid": "claimlink",
|
"modid": "claimlink",
|
||||||
"name": "ClaimLink",
|
"name": "ClaimLink",
|
||||||
|
"side": "Server",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Unknown"
|
"anth64"
|
||||||
],
|
],
|
||||||
"description": "To be added",
|
"description": "Link claims together with groups.",
|
||||||
"version": "1.0.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"game": "1.21.0"
|
"game": "1.22.5",
|
||||||
|
"commandhook": "2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user