Compare commits

..
29 Commits
Author SHA1 Message Date
anth64 636a7dee98 feat: filter buffer data sent to visualizer clients by view distance 2026-07-31 07:38:56 +02:00
anth64 1e4400555c chore: bump version to 0.2.0 2026-07-31 07:18:56 +02:00
anth64 0dfcc5396a feat: add client-visualizer network endpoint for buffer geometry 2026-07-30 23:14:06 +02:00
anth64 c110ee2604 fix: normalize claim geometry before growing buffer (raw start/end marks aren't min/max ordered) 2026-07-27 07:40:12 +02:00
anth64 baf3ccbd93 feat: enforce buffer-zone non-overlap on /claimlink link 2026-07-26 15:30:35 +02:00
anth64 3cf6db1d2a fix: clear PendingLoads on /land claim new to match vanilla's unconditional reset 2026-07-26 14:36:37 +02:00
anth64 12219e6ef5 feat: add ClaimLinkConfig with configurable buffer size 2026-07-26 12:07:46 +02:00
anth64 0e0e4e0f74 chore: bump version to 0.1.0 2026-07-26 11:28:08 +02:00
anth64 154e7b7848 chore: bump version to 0.0.7 2026-07-26 11:25:27 +02:00
anth64 9e37ec11cf feat: implement admin delete/unlink/kick, fix admin unlink claim arg type 2026-07-26 11:23:21 +02:00
anth64 4d98a115ed feat: infer claim link group from chat context when omitted; drop redundant admin info 2026-07-26 11:21:13 +02:00
anth64 99238c4744 feat: hook group leave/kick to proactively cancel pending actions with a reason 2026-07-26 10:33:46 +02:00
anth64 1c079b7e44 fix: cancel pending actions on group disband even if not yet a claim link 2026-07-26 09:08:08 +02:00
anth64 b7ca9fa53b fix: track groupId per pending action for precise cleanup on delete/disband; split group hook into its own listener; self-heal orphaned registry entries in list 2026-07-26 08:51:54 +02:00
anth64 c55787ce40 feat: react to /group confirmdisband and recheck live membership on link confirm 2026-07-23 07:57:39 +02:00
anth64 b10d3a0f55 fix: cancel old owner's pending action on ownership transfer 2026-07-21 20:29:32 +02:00
anth64 b5ceba442a fix: cancel kicked player's pending action to stop stale link re-add 2026-07-21 20:25:21 +02:00
anth64 d248b772dc fix: split adminfree (by player name) from adminfreehere (by position) 2026-07-21 19:36:48 +02:00
anth64 c8fc86069b feat: gate claim link info detail by membership/admin, count members with no claims 2026-07-20 23:36:03 +02:00
anth64 2e4cc25b20 fix: guard claim-link entries against stale indices and dead groups 2026-07-20 23:28:37 +02:00
anth64 fc7e601f5b refactor: expose TryResolveOwnedClaim for future external mod consumption 2026-07-19 20:43:05 +02:00
anth64 a2e4a68f9e refactor: rebuild claim link registry around live index tracking, drop ClaimLink type 2026-07-19 20:31:10 +02:00
anth64 741d283f38 refactor: clear pending actions in claim link command instead of land claim listener 2026-07-19 10:49:11 +02:00
anth64 9fcf6f1da5 fix: resolve claim on load 2026-07-19 10:48:41 +02:00
anth64 f699cd6d54 chore: log adminfree phase and claims found at caller position 2026-07-18 11:55:24 +02:00
anth64 94b6723456 feat: sanity-check claim save/free against tracked ordering state 2026-07-18 11:47:52 +02:00
anth64 de2026d239 refactor: merge land command Before/After into shared phase-aware dispatch 2026-07-18 11:46:32 +02:00
anth64 fffc046126 refactor: normalize claim link membership into flat entry storage 2026-07-15 18:46:52 +02:00
anth64 eaa79f8f52 fix: transferownership correctly sets owner level and demotes old owner to op, works for offline players, notifies members 2026-07-14 22:52:32 +02:00
10 changed files with 1371 additions and 355 deletions
+13 -10
View File
@@ -4,21 +4,24 @@ using ProtoBuf;
namespace ClaimLink; namespace ClaimLink;
[ProtoContract] [ProtoContract]
public class ClaimLinkMember public class ClaimLinkSaveData
{
[ProtoMember(1)]
public List<int> Links = new();
[ProtoMember(2)]
public List<ClaimLinkEntrySaveData> Entries = new();
}
[ProtoContract]
public class ClaimLinkEntrySaveData
{ {
[ProtoMember(1)] [ProtoMember(1)]
public string OwnerPlayerUid = ""; public string OwnerPlayerUid = "";
[ProtoMember(2)] [ProtoMember(2)]
public List<int> LocalClaimIndices = new();
}
[ProtoContract]
public class ClaimLink
{
[ProtoMember(1)]
public int GroupId; public int GroupId;
[ProtoMember(2)] [ProtoMember(3)]
public List<ClaimLinkMember> Members = new(); public List<int> ClaimIndicies = new();
} }
+140
View File
@@ -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);
}
}
+462 -152
View File
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Vintagestory.API.Common; using Vintagestory.API.Common;
using Vintagestory.API.Config;
using Vintagestory.API.MathTools;
using Vintagestory.API.Server; using Vintagestory.API.Server;
namespace ClaimLink; namespace ClaimLink;
@@ -68,14 +70,18 @@ public static class ClaimLinkChatCommand
new( new(
new[] { "new", "n" }, new[] { "new", "n" },
"Promote a group you own to a claim link.", "Promote a group you own to a claim link.",
new ICommandArgumentParser[] { p.Word("groupname") }, new ICommandArgumentParser[] { p.OptionalWord("groupname") },
true, true,
New New
), ),
new( new(
new[] { "link", "l" }, new[] { "link", "l" },
"Link a claim you own to a claim link.", "Link a claim you own to a claim link.",
new ICommandArgumentParser[] { p.Word("groupname"), p.IntRange("claim", 0, 999) }, new ICommandArgumentParser[]
{
p.IntRange("claim", 0, 999),
p.OptionalWord("groupname"),
},
true, true,
Link Link
), ),
@@ -103,28 +109,28 @@ public static class ClaimLinkChatCommand
new( new(
new[] { "kick" }, new[] { "kick" },
"Force-unlink all of a player's claims from the claim link.", "Force-unlink all of a player's claims from the claim link.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") },
true, true,
Kick Kick
), ),
new( new(
new[] { "delete" }, new[] { "delete" },
"Delete a claim link.", "Delete a claim link.",
new ICommandArgumentParser[] { p.Word("groupname") }, new ICommandArgumentParser[] { p.OptionalWord("groupname") },
true, true,
Delete Delete
), ),
new( new(
new[] { "transferownership", "transfer", "to" }, new[] { "transferownership", "transfer", "to" },
"Transfer ownership of the claim link to another player.", "Transfer ownership of the claim link to another player.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") },
true, true,
TransferOwnership TransferOwnership
), ),
new( new(
new[] { "info", "i" }, new[] { "info", "i" },
"Show a claim link's members and linked claims.", "Show a claim link's members and linked claims.",
new ICommandArgumentParser[] { p.Word("groupname") }, new ICommandArgumentParser[] { p.OptionalWord("groupname") },
true, true,
Info Info
), ),
@@ -149,38 +155,35 @@ public static class ClaimLinkChatCommand
new( new(
new[] { "delete", "del" }, new[] { "delete", "del" },
"Delete a claim link.", "Delete a claim link.",
new ICommandArgumentParser[] { p.Word("groupname") }, new ICommandArgumentParser[] { p.OptionalWord("groupname") },
false, false,
AdminDelete AdminDelete
), ),
new( new(
new[] { "unlink" }, new[] { "unlink" },
"Unlink a claim.", "Unlink a claim.",
new ICommandArgumentParser[] { p.OnlinePlayer("playername"), p.Word("claim") }, new ICommandArgumentParser[] { p.Word("playername"), p.IntRange("claim", 0, 999) },
false, false,
AdminUnlink AdminUnlink
), ),
new( new(
new[] { "kick" }, new[] { "kick" },
"Force-unlink all of a player's claims.", "Force-unlink all of a player's claims.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, new ICommandArgumentParser[] { p.Word("groupname"), p.OptionalWord("playername") },
false, false,
AdminKick AdminKick
), ),
new( new(
new[] { "transferownership", "transfer", "to" }, new[] { "transferownership", "transfer", "to" },
"Transfer ownership of any claim link to another player.", "Transfer ownership of any claim link to another player.",
new ICommandArgumentParser[] { p.Word("groupname"), p.OnlinePlayer("playername") }, new ICommandArgumentParser[]
{
p.PlayerUids("playername"),
p.OptionalWord("groupname"),
},
false, false,
AdminTransferOwnership 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) foreach (var spec in adminCommands)
@@ -189,22 +192,53 @@ public static class ClaimLinkChatCommand
admin.EndSubCommand(); admin.EndSubCommand();
} }
private static readonly Dictionary<string, Func<TextCommandResult>> pendingActions = new(); internal static readonly Dictionary<
string,
(int GroupId, Func<TextCommandResult> Action)
> PendingActions = new();
internal static void RemovePending(string playerUid) => pendingActions.Remove(playerUid); internal static readonly Dictionary<string, string> LastCancelReason = new();
private static TextCommandResult Stage( private static TextCommandResult Stage(
string playerUid, string playerUid,
int groupId,
string prompt, string prompt,
Func<TextCommandResult> action Func<TextCommandResult> action
) )
{ {
pendingActions[playerUid] = action; PendingActions[playerUid] = (groupId, action);
LastCancelReason.Remove(playerUid);
return TextCommandResult.Success( return TextCommandResult.Success(
$"{prompt} Use /claimlink confirm to proceed, or /claimlink cancel to cancel." $"{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) private static TextCommandResult? TryResolveGroup(string groupName, out PlayerGroup group)
{ {
group = ClaimLinkModSystem.Groups.GetPlayerGroupByName(groupName)!; group = ClaimLinkModSystem.Groups.GetPlayerGroupByName(groupName)!;
@@ -213,10 +247,27 @@ public static class ClaimLinkChatCommand
: null; : null;
} }
private static TextCommandResult? TryResolveClaimLink(PlayerGroup group, out ClaimLink link) private static TextCommandResult? TryResolveGroupArg(
TextCommandCallingArgs args,
int argIndex,
out PlayerGroup group
)
{ {
link = ClaimLinkModSystem.Registry.Get(group.Uid)!; if (!args.Parsers[argIndex].IsMissing)
return link == null 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.") ? TextCommandResult.Error($"'{group.Name}' is not a claim link.")
: null; : null;
} }
@@ -248,10 +299,9 @@ public static class ClaimLinkChatCommand
public static TextCommandResult New(TextCommandCallingArgs args) public static TextCommandResult New(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0];
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group); TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group);
if (err != null) if (err != null)
return err; return err;
@@ -259,16 +309,21 @@ public static class ClaimLinkChatCommand
if (err != null) if (err != null)
return err; return err;
if (ClaimLinkModSystem.Registry.Get(group.Uid) != null) if (ClaimLinkModSystem.Registry.Exists(group.Uid))
return TextCommandResult.Error($"'{groupName}' is already a claim link."); return TextCommandResult.Error($"'{group.Name}' is already a claim link.");
int groupId = group.Uid; int groupId = group.Uid;
string groupName = group.Name;
return Stage( return Stage(
playerUid, playerUid,
groupId,
$"'{groupName}' will become a claim link.", $"'{groupName}' will become a claim link.",
() => () =>
{ {
ClaimLinkModSystem.Registry.Add(new ClaimLink { GroupId = groupId }); 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."); return TextCommandResult.Success($"'{groupName}' is now a claim link.");
} }
); );
@@ -278,18 +333,23 @@ public static class ClaimLinkChatCommand
{ {
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
if (!pendingActions.TryGetValue(playerUid, out Func<TextCommandResult>? action)) 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."); return TextCommandResult.Error("You do not have a pending action.");
}
pendingActions.Remove(playerUid); PendingActions.Remove(playerUid);
return action(); LastCancelReason.Remove(playerUid);
return pending.Action();
} }
public static TextCommandResult Cancel(TextCommandCallingArgs args) public static TextCommandResult Cancel(TextCommandCallingArgs args)
{ {
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
if (!pendingActions.Remove(playerUid)) if (!PendingActions.Remove(playerUid))
return TextCommandResult.Error("You do not have a pending action."); return TextCommandResult.Error("You do not have a pending action.");
return TextCommandResult.Success("Pending action cancelled."); return TextCommandResult.Success("Pending action cancelled.");
@@ -297,14 +357,13 @@ public static class ClaimLinkChatCommand
public static TextCommandResult Link(TextCommandCallingArgs args) public static TextCommandResult Link(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0]; int claimIndex = (int)args[0];
int claimIndex = (int)args[1];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group); TextCommandResult? err = TryResolveGroupArg(args, 1, out PlayerGroup group);
if (err != null) if (err != null)
return err; return err;
err = TryResolveClaimLink(group, out ClaimLink link); err = TryResolveClaimLink(group);
if (err != null) if (err != null)
return err; return err;
@@ -314,30 +373,74 @@ public static class ClaimLinkChatCommand
return err; return err;
string playerUid = player.PlayerUID; string playerUid = player.PlayerUID;
int groupId = group.Uid;
string groupName = group.Name;
if (!ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out _, out _)) if (
return TextCommandResult.Error("You do not own that claim."); !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)) if (ClaimLinkModSystem.Registry.IsClaimLinked(playerUid, claimIndex))
return TextCommandResult.Error("That claim is already part of a claim link."); 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); string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage( return Stage(
playerUid, playerUid,
$"{claimDesc} will be linked into '{groupName}'.", groupId,
$"'{claimDesc}' will be linked into '{groupName}'.",
() => () =>
{ {
ClaimLinkMember? member = link.Members.Find(m => m.OwnerPlayerUid == playerUid); bool stillMember =
if (member == null) ClaimLinkModSystem
{ .PlayerData.GetPlayerDataByUid(playerUid)
member = new ClaimLinkMember { OwnerPlayerUid = playerUid }; ?.PlayerGroupMemberships.ContainsKey(groupId) == true;
link.Members.Add(member); if (!stillMember)
} return TextCommandResult.Error($"You are no longer a member of '{groupName}'.");
member.LocalClaimIndices.Add(claimIndex); bool success = ClaimLinkModSystem.Registry.AddEntry(playerUid, groupId, claimIndex);
ClaimLinkModSystem.Registry.Save(); return success
? TextCommandResult.Success($"Linked '{claimDesc}' to '{groupName}'.")
return TextCommandResult.Success($"Linked {claimDesc} to '{groupName}'."); : TextCommandResult.Error(
$"Unable to link claim {claimIndex} to '{groupName}', it is currently being modified."
);
} }
); );
} }
@@ -347,68 +450,83 @@ public static class ClaimLinkChatCommand
int claimIndex = (int)args[0]; int claimIndex = (int)args[0];
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
ClaimLink? link = ClaimLinkModSystem.Registry.FindLinkContaining(playerUid, claimIndex); int? groupId = ClaimLinkModSystem.Registry.FindGroupContaining(playerUid, claimIndex);
if (link == null) if (groupId == null)
return TextCommandResult.Error( return TextCommandResult.Error(
$"Claim {claimIndex} is not linked into any claim link by you." $"Claim {claimIndex} is not linked into any claim link by you."
); );
string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[link.GroupId].Name; string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[(int)groupId].Name;
string claimDesc = DescribeClaim(playerUid, claimIndex); string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage( return Stage(
playerUid, playerUid,
$"{claimDesc} will be unlinked from '{groupName}'.", (int)groupId,
$"'{claimDesc}' will be unlinked from '{groupName}'.",
() => () =>
{ {
ClaimLinkMember member = link.Members.Find(m => m.OwnerPlayerUid == playerUid)!; ClaimLinkModSystem.Registry.RemoveEntry(playerUid, (int)groupId, claimIndex);
member.LocalClaimIndices.Remove(claimIndex); return TextCommandResult.Success($"Unlinked '{claimDesc}' from '{groupName}'.");
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) public static TextCommandResult Kick(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0]; string word1 = (string)args[0];
IPlayer target = (IPlayer)args[1]; string? word2 = args.Parsers[1].IsMissing ? null : (string)args[1];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group); string targetName;
if (err != null) PlayerGroup group;
return err; 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;
}
err = TryResolveClaimLink(group, out ClaimLink link); TextCommandResult? linkErr = TryResolveClaimLink(group);
if (err != null) if (linkErr != null)
return err; return linkErr;
err = RequireOpOrOwner(args.Caller.Player, group); linkErr = RequireOpOrOwner(args.Caller.Player, group);
if (err != null) if (linkErr != null)
return err; return linkErr;
string? targetUid = ClaimLinkModSystem
.PlayerData.GetPlayerDataByLastKnownName(targetName)
?.PlayerUID;
if (targetUid == null)
return TextCommandResult.Error($"No such player '{targetName}'.");
string targetUid = target.PlayerUID;
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
string groupName = group.Name;
ClaimLinkMember? member = link.Members.Find(m => m.OwnerPlayerUid == targetUid); if (!ClaimLinkModSystem.Registry.HasAnyEntry(targetUid, group.Uid))
if (member == null) return TextCommandResult.Error($"{targetName} has no claims linked in '{groupName}'.");
return TextCommandResult.Error(
$"{target.PlayerName} has no claims linked in '{groupName}'."
);
return Stage( return Stage(
playerUid, playerUid,
$"All of {target.PlayerName}'s claims will be unlinked from '{groupName}'.", group.Uid,
$"All of {targetName}'s claims will be unlinked from '{groupName}'.",
() => () =>
{ {
link.Members.Remove(member); ClaimLinkModSystem.Registry.RemoveAllForPlayerInGroup(targetUid, group.Uid);
RemovePending(targetUid); CommandListener.PendingLoads.Remove(targetUid);
ClaimLinkModSystem.Registry.Save(); PendingActions.Remove(targetUid);
return TextCommandResult.Success( return TextCommandResult.Success(
$"Unlinked all claims of {target.PlayerName} from '{groupName}'." $"Unlinked all claims of {targetName} from '{groupName}'."
); );
} }
); );
@@ -416,14 +534,13 @@ public static class ClaimLinkChatCommand
public static TextCommandResult Delete(TextCommandCallingArgs args) public static TextCommandResult Delete(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0];
string playerUid = args.Caller.Player.PlayerUID; string playerUid = args.Caller.Player.PlayerUID;
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group); TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group);
if (err != null) if (err != null)
return err; return err;
err = TryResolveClaimLink(group, out ClaimLink link); err = TryResolveClaimLink(group);
if (err != null) if (err != null)
return err; return err;
@@ -432,14 +549,14 @@ public static class ClaimLinkChatCommand
return err; return err;
int groupId = group.Uid; int groupId = group.Uid;
string groupName = group.Name;
return Stage( return Stage(
playerUid, playerUid,
groupId,
$"'{groupName}' will be deleted as a claim link.", $"'{groupName}' will be deleted as a claim link.",
() => () =>
{ {
foreach (ClaimLinkMember member in link.Members) RemovePendingActionsForGroup(groupId, "claim link was deleted");
RemovePending(member.OwnerPlayerUid);
ClaimLinkModSystem.Registry.Remove(groupId); ClaimLinkModSystem.Registry.Remove(groupId);
return TextCommandResult.Success($"'{groupName}' is no longer a claim link."); return TextCommandResult.Success($"'{groupName}' is no longer a claim link.");
} }
@@ -448,16 +565,36 @@ public static class ClaimLinkChatCommand
public static TextCommandResult TransferOwnership(TextCommandCallingArgs args) public static TextCommandResult TransferOwnership(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0]; string word1 = (string)args[0];
IPlayer target = (IPlayer)args[1]; 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; IPlayer player = args.Caller.Player;
string playerUid = player.PlayerUID; string playerUid = player.PlayerUID;
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group); TextCommandResult? err = TryResolveOnlinePlayerByName(targetName, out IPlayer target);
if (err != null) if (err != null)
return err; return err;
err = TryResolveClaimLink(group, out _); err = TryResolveClaimLink(group);
if (err != null) if (err != null)
return err; return err;
@@ -473,13 +610,49 @@ public static class ClaimLinkChatCommand
if (err != null) if (err != null)
return err; return err;
string groupName = group.Name;
return Stage( return Stage(
playerUid, playerUid,
group.Uid,
$"Ownership of '{groupName}' will be transferred to {target.PlayerName}.", $"Ownership of '{groupName}' will be transferred to {target.PlayerName}.",
() => ExecuteTransferOwnership(group, target) () => 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) private static TextCommandResult? RequireNotAlreadyOwner(PlayerGroup group, IPlayer target)
{ {
return target.PlayerUID == group.OwnerUID return target.PlayerUID == group.OwnerUID
@@ -490,120 +663,255 @@ public static class ClaimLinkChatCommand
private static TextCommandResult ExecuteTransferOwnership(PlayerGroup group, IPlayer target) private static TextCommandResult ExecuteTransferOwnership(PlayerGroup group, IPlayer target)
{ {
string oldOwnerUid = group.OwnerUID; string oldOwnerUid = group.OwnerUID;
string targetUid = target.PlayerUID;
IPlayer? oldOwner = ClaimLinkModSystem.World.PlayerByUid(oldOwnerUid); IPlayer? oldOwner = ClaimLinkModSystem.World.PlayerByUid(oldOwnerUid);
EnumPlayerGroupMemberShip? oldOwnerLevel = oldOwner?.GetGroup(group.Uid)?.Level; PlayerGroupMembership? oldOwnerMembership = oldOwner?.GetGroup(group.Uid);
EnumPlayerGroupMemberShip targetLevel = target.GetGroup(group.Uid)!.Level; PlayerGroupMembership targetMembership = target.GetGroup(group.Uid)!;
// TODO remove debug logging once transfer ownership persistence is confirmed tested group.OwnerUID = target.PlayerUID;
ClaimLinkModSystem.Logger.Notification( targetMembership.Level = EnumPlayerGroupMemberShip.Owner;
$"claimlink: transferownership '{group.Name}' (uid {group.Uid}) BEFORE OwnerUID={oldOwnerUid} "
+ $"(old owner {oldOwner?.PlayerName ?? oldOwnerUid} level={oldOwnerLevel}, new owner {target.PlayerName}/{targetUid} level={targetLevel})"
);
group.OwnerUID = targetUid; if (oldOwnerMembership != null)
oldOwnerMembership.Level = EnumPlayerGroupMemberShip.Op;
ClaimLinkModSystem.Logger.Notification( PendingActions.Remove(oldOwnerUid);
$"claimlink: transferownership '{group.Name}' (uid {group.Uid}) AFTER OwnerUID={group.OwnerUID}"
);
return TextCommandResult.Success($"'{group.Name}' is now owned by {target.PlayerName}."); 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) public static TextCommandResult Info(TextCommandCallingArgs args)
{ {
string groupName = (string)args[0]; TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group);
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) if (err != null)
return err; return err;
err = TryResolveClaimLink(group, out ClaimLink link); err = TryResolveClaimLink(group);
if (err != null) if (err != null)
return err; return err;
return TextCommandResult.Success(FormatInfo(groupName, link)); 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) public static TextCommandResult List(TextCommandCallingArgs args)
{ {
List<ClaimLink> links = ClaimLinkModSystem List<int> groupIds = new(ClaimLinkModSystem.Registry.All);
.Registry.All.OrderByDescending(l => l.Members.Count) groupIds.Sort((a, b) => GroupMemberCount(b).CompareTo(GroupMemberCount(a)));
.ToList();
if (links.Count == 0) if (groupIds.Count == 0)
return TextCommandResult.Success("There are no claim links."); return TextCommandResult.Success("There are no claim links.");
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine($"Claim links ({links.Count}):"); int shown = 0;
foreach (ClaimLink link in links) foreach (int groupId in groupIds)
{ {
string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[link.GroupId].Name; if (
sb.AppendLine( !ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue(
$" {groupName}: {link.Members.Count} member{(link.Members.Count == 1 ? "" : "s")}" 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++;
} }
return TextCommandResult.Success(sb.ToString()); if (shown == 0)
return TextCommandResult.Success("There are no claim links.");
return TextCommandResult.Success($"Claim links ({shown}):\n{sb}");
} }
private static string DescribeClaim(string ownerPlayerUid, int localIndex) 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 ( if (
!ClaimLinkModSystem.TryResolveOwnedClaim( !ClaimLinkModSystem.TryResolveOwnedClaim(
ownerPlayerUid, ownerPlayerUid,
localIndex, claimIndex,
out _,
out LandClaim? claim out LandClaim? claim
) )
|| claim == null || claim == null
) )
return $"claim {localIndex}"; return $"claim {claimIndex}";
return string.IsNullOrEmpty(claim.Description) ? $"claim {localIndex}" : claim.Description; return string.IsNullOrEmpty(claim.Description) ? $"claim {claimIndex}" : claim.Description;
} }
private static string FormatInfo(string groupName, ClaimLink link) private static string FormatInfo(string groupName, int groupId, bool showDetails)
{ {
int memberCount = GroupMemberCount(groupId);
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine( sb.AppendLine(
$"Claim link '{groupName}' ({link.Members.Count} member{(link.Members.Count == 1 ? "" : "s")}):" $"Claim link '{groupName}' ({memberCount} member{(memberCount == 1 ? "" : "s")}):"
); );
foreach (ClaimLinkMember member in link.Members) foreach (string uid in GroupMemberUids(groupId))
{ {
string name = string name = ClaimLinkModSystem.World.PlayerByUid(uid)?.PlayerName ?? uid;
ClaimLinkModSystem.World.PlayerByUid(member.OwnerPlayerUid)?.PlayerName
?? member.OwnerPlayerUid; if (!showDetails)
IEnumerable<string> claims = member.LocalClaimIndices.Select(i => {
DescribeClaim(member.OwnerPlayerUid, i) sb.AppendLine($" {name}");
); continue;
}
IEnumerable<string> claims = ClaimLinkModSystem
.Registry.ClaimsForPlayerInGroup(uid, groupId)
.Select(i => DescribeClaim(uid, i));
sb.AppendLine($" {name}: [{string.Join(", ", claims)}]"); sb.AppendLine($" {name}: [{string.Join(", ", claims)}]");
} }
return sb.ToString(); return sb.ToString();
} }
public static TextCommandResult AdminDelete(TextCommandCallingArgs args) => 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)
{ {
string groupName = (string)args[0]; TextCommandResult? err = TryResolveGroupArg(args, 0, out PlayerGroup group);
IPlayer target = (IPlayer)args[1];
TextCommandResult? err = TryResolveGroup(groupName, out PlayerGroup group);
if (err != null) if (err != null)
return err; return err;
err = TryResolveClaimLink(group, out _); 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) if (err != null)
return err; return err;
@@ -618,6 +926,8 @@ public static class ClaimLinkChatCommand
return ExecuteTransferOwnership(group, target); return ExecuteTransferOwnership(group, target);
} }
public static TextCommandResult AdminInfo(TextCommandCallingArgs args) => internal static void OnPlayerDisconnect(IServerPlayer player)
TextCommandResult.Success("stub: claimlink admin info"); {
PendingActions.Remove(player.PlayerUID);
}
} }
+6
View File
@@ -0,0 +1,6 @@
namespace ClaimLink;
internal class ClaimLinkConfig
{
public int BufferSize = 16;
}
+63 -29
View File
@@ -12,60 +12,94 @@ public class ClaimLinkModSystem : ModSystem
internal static ClaimLinkRegistry Registry = null!; internal static ClaimLinkRegistry Registry = null!;
internal static IGroupManager Groups = null!; internal static IGroupManager Groups = null!;
internal static IWorldAccessor World = null!; internal static IWorldAccessor World = null!;
internal ClaimLinkCommandListener? cmdListener; 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 bool ShouldLoad(EnumAppSide forSide) => forSide == EnumAppSide.Server;
public override void StartServerSide(ICoreServerAPI api) public override void StartServerSide(ICoreServerAPI api)
{ {
LandClaimAPI = api.World.Claims; LandClaimAPI = api.World.Claims;
Logger = api.Logger;
Registry = new ClaimLinkRegistry(api.WorldManager.SaveGame);
Groups = api.Groups;
World = api.World; 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 ClaimLinkCommandListener(); CmdListener = new CommandListener();
CommandHookModSystem.Register(cmdListener); CommandHookModSystem.Register(CmdListener);
GroupCmdListener = new GroupCommandListener();
CommandHookModSystem.Register(GroupCmdListener);
ClaimLinkChatCommand.Register(api); ClaimLinkChatCommand.Register(api);
ClaimLinkVisualizerNetwork.Start(api);
api.Event.PlayerDisconnect += ClaimLinkCommandListener.OnPlayerDisconnect; api.Event.PlayerDisconnect += CommandListener.OnPlayerDisconnect;
api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect;
// TODO remove debug logging once transfer ownership persistence is confirmed tested api.Event.PlayerDisconnect += ClaimLinkChatCommand.OnPlayerDisconnect;
foreach (ClaimLink link in Registry.All)
{
PlayerGroup group = Groups.PlayerGroupsById[link.GroupId];
Logger.Notification($"claimlink: startup state '{group.Name}' (uid {group.Uid}) OwnerUID={group.OwnerUID}");
}
} }
public override void Dispose() public override void Dispose()
{ {
if (cmdListener != null) if (CmdListener != null)
CommandHookModSystem.Unregister(cmdListener); CommandHookModSystem.Unregister(CmdListener);
if (GroupCmdListener != null)
CommandHookModSystem.Unregister(GroupCmdListener);
} }
internal static bool TryResolveOwnedClaim(string ownerPlayerUid, int localIndex, out int globalIndex, out LandClaim? claim) private static IEnumerable<(int claimIndex, LandClaim claim)> EnumerateOwnedClaims(
string ownerPlayerUid
)
{ {
globalIndex = -1; int index = 0;
claim = null; foreach (LandClaim c in LandClaimAPI.All)
List<LandClaim> claims = LandClaimAPI.All;
int count = 0;
for (int i = 0; i < claims.Count; ++i)
{ {
if (claims[i].OwnedByPlayerUid != ownerPlayerUid) if (c.OwnedByPlayerUid != ownerPlayerUid)
continue; continue;
if (count == localIndex) 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)
{ {
globalIndex = i; claim = c;
claim = claims[i];
return true; return true;
} }
count++;
} }
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; return false;
} }
} }
+162 -26
View File
@@ -1,65 +1,201 @@
using System.Collections.Generic; using System.Collections.Generic;
using ProtoBuf;
using Vintagestory.API.Server; using Vintagestory.API.Server;
namespace ClaimLink; namespace ClaimLink;
[ProtoContract]
public class ClaimLinkData
{
[ProtoMember(1)]
public List<ClaimLink> Links = new();
}
public class ClaimLinkRegistry public class ClaimLinkRegistry
{ {
private const string SaveKey = "claimlink:links"; private const string SaveKey = "claimlink:claimlinkdata";
private readonly ISaveGame saveGame; private readonly ISaveGame saveGame;
private readonly Dictionary<int, ClaimLink> byGroupId = new();
private readonly List<int> claimLinks = new();
private readonly Dictionary<string, Dictionary<int, List<int>>> entries = new();
public ClaimLinkRegistry(ISaveGame saveGame) public ClaimLinkRegistry(ISaveGame saveGame)
{ {
this.saveGame = saveGame; this.saveGame = saveGame;
ClaimLinkData? data = saveGame.GetData<ClaimLinkData?>(SaveKey, null); ClaimLinkSaveData? data = saveGame.GetData<ClaimLinkSaveData?>(SaveKey, null);
if (data == null) if (data == null)
return; return;
foreach (ClaimLink link in data.Links) claimLinks.AddRange(data.Links);
byGroupId[link.GroupId] = link;
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 ClaimLink? Get(int groupId) => byGroupId.TryGetValue(groupId, out ClaimLink? link) ? link : null; public IReadOnlyList<int> All => claimLinks;
public IReadOnlyCollection<ClaimLink> All => byGroupId.Values; public bool Exists(int groupId) => claimLinks.Contains(groupId);
public void Add(ClaimLink link) public void Add(int groupId)
{ {
byGroupId[link.GroupId] = link; claimLinks.Add(groupId);
Save(); Save();
} }
public void Remove(int groupId) public void Remove(int groupId)
{ {
byGroupId.Remove(groupId); claimLinks.Remove(groupId);
foreach (var groups in entries.Values)
groups.Remove(groupId);
Prune();
Save(); Save();
} }
public bool IsClaimLinked(string ownerPlayerUid, int localIndex) => FindLinkContaining(ownerPlayerUid, localIndex) != null; public bool IsClaimLinked(string uid, int claimIndex) =>
FindGroupContaining(uid, claimIndex) != null;
public ClaimLink? FindLinkContaining(string ownerPlayerUid, int localIndex) public int? FindGroupContaining(string uid, int claimIndex)
{ {
foreach (ClaimLink link in byGroupId.Values) if (!entries.TryGetValue(uid, out var groups))
foreach (ClaimLinkMember member in link.Members) return null;
if (member.OwnerPlayerUid == ownerPlayerUid && member.LocalClaimIndices.Contains(localIndex)) foreach (var (groupId, claims) in groups)
return link; if (claims.Contains(claimIndex))
return groupId;
return null; 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() public void Save()
{ {
saveGame.StoreData(SaveKey, new ClaimLinkData { Links = new List<ClaimLink>(byGroupId.Values) }); 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 }
);
} }
} }
+107
View File
@@ -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;
}
+416
View File
@@ -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);
}
}
-136
View File
@@ -1,136 +0,0 @@
using Vintagestory.API.Common;
using System.Collections.Generic;
using CommandHook;
using Vintagestory.API.Server;
namespace ClaimLink;
public class ClaimLinkCommandListener : ICommandHookListener
{
public string ModId => "claimlink";
public IReadOnlyList<string> Commands => new[] { "land" };
public CommandRegistration Registration => new(Before, After);
private static Dictionary<string, int> pendingLoads = new Dictionary<string, int>();
private delegate void SubHandler(Caller caller, CmdArgs args);
private static readonly Dictionary<string, SubHandler> topLevel = new()
{
["claim"] = Claim,
["free"] = Free,
["info"] = Info,
["list"] = List,
["adminfree"] = AdminFree,
};
private static readonly Dictionary<string, SubHandler> claimSub = new()
{
["load"] = ClaimLoad,
["new"] = ClaimNew,
["grant"] = ClaimGrant,
["revoke"] = ClaimRevoke,
["grantgroup"] = ClaimGrantGroup,
["revokegroup"] = ClaimRevokeGroup,
["start"] = ClaimStart,
["end"] = ClaimEnd,
["grow"] = ClaimGrow,
["shrink"] = ClaimShrink,
["add"] = ClaimAdd,
["allowuseeveryone"] = ClaimAllowUseEveryone,
["plevel"] = ClaimPLevel,
["fullheight"] = ClaimFullHeight,
["save"] = ClaimSave,
["cancel"] = ClaimCancel,
["download"] = ClaimDownload,
};
private TextCommandResult? Before(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();
}
}
return null;
}
private void After(TextCommandCallingArgs args, TextCommandResult result)
{
}
private static void Claim(Caller caller, CmdArgs args)
{
string word = args.PeekWord();
if (claimSub.TryGetValue(word, out SubHandler? handler))
{
args.PopWord();
handler(caller, args);
}
}
private static void ClaimLoad(Caller caller, CmdArgs args)
{
if (caller.Player == null)
return;
int? claimIndex = args.PopInt();
if (claimIndex == null || claimIndex < 0 || claimIndex > 999)
return;
if (!ClaimLinkModSystem.TryResolveOwnedClaim(caller.Player.PlayerUID, (int)claimIndex, out int globalIndex, out _))
return;
pendingLoads[caller.Player.PlayerUID] = globalIndex;
}
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) { }
private static void Info(Caller caller, CmdArgs args) { }
private static void List(Caller caller, CmdArgs args) { }
private static void AdminFree(Caller caller, CmdArgs args) { }
private static void ClaimNew(Caller caller, CmdArgs args) { }
private static void ClaimGrant(Caller caller, CmdArgs args) { }
private static void ClaimRevoke(Caller caller, CmdArgs args) { }
private static void ClaimGrantGroup(Caller caller, CmdArgs args) { }
private static void ClaimRevokeGroup(Caller caller, CmdArgs args) { }
private static void ClaimStart(Caller caller, CmdArgs args) { }
private static void ClaimEnd(Caller caller, CmdArgs args) { }
private static void ClaimGrow(Caller caller, CmdArgs args) { }
private static void ClaimShrink(Caller caller, CmdArgs args) { }
private static void ClaimAdd(Caller caller, CmdArgs args) { }
private static void ClaimAllowUseEveryone(Caller caller, CmdArgs args) { }
private static void ClaimPLevel(Caller caller, CmdArgs args) { }
private static void ClaimFullHeight(Caller caller, CmdArgs args) { }
private static void ClaimSave(Caller caller, CmdArgs args) { }
private static void ClaimDownload(Caller caller, CmdArgs args) { }
internal static void OnPlayerDisconnect(IServerPlayer player)
{
pendingLoads.Remove(player.PlayerUID);
ClaimLinkChatCommand.RemovePending(player.PlayerUID);
}
}
+2 -2
View File
@@ -8,9 +8,9 @@
"anth64" "anth64"
], ],
"description": "Link claims together with groups.", "description": "Link claims together with groups.",
"version": "0.0.6", "version": "0.3.0",
"dependencies": { "dependencies": {
"game": "1.22.3", "game": "1.22.5",
"commandhook": "2.1.0" "commandhook": "2.1.0"
} }
} }