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

This commit is contained in:
2026-07-26 08:51:54 +02:00
parent c55787ce40
commit b7ca9fa53b
5 changed files with 123 additions and 68 deletions
+49 -12
View File
@@ -190,20 +190,38 @@ public static class ClaimLinkChatCommand
admin.EndSubCommand(); admin.EndSubCommand();
} }
internal static readonly Dictionary<string, Func<TextCommandResult>> PendingActions = new(); internal static readonly Dictionary<
string,
(int GroupId, Func<TextCommandResult> Action)
> PendingActions = 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);
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)
{
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);
}
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)!;
@@ -263,6 +281,7 @@ public static class ClaimLinkChatCommand
int groupId = group.Uid; int groupId = group.Uid;
return Stage( return Stage(
playerUid, playerUid,
groupId,
$"'{groupName}' will become a claim link.", $"'{groupName}' will become a claim link.",
() => () =>
{ {
@@ -276,11 +295,11 @@ 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))
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(); return pending.Action();
} }
public static TextCommandResult Cancel(TextCommandCallingArgs args) public static TextCommandResult Cancel(TextCommandCallingArgs args)
@@ -323,6 +342,7 @@ public static class ClaimLinkChatCommand
string claimDesc = DescribeClaim(playerUid, claimIndex); string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage( return Stage(
playerUid, playerUid,
groupId,
$"{claimDesc} will be linked into '{groupName}'.", $"{claimDesc} will be linked into '{groupName}'.",
() => () =>
{ {
@@ -359,6 +379,7 @@ public static class ClaimLinkChatCommand
return Stage( return Stage(
playerUid, playerUid,
(int)groupId,
$"{claimDesc} will be unlinked from '{groupName}'.", $"{claimDesc} will be unlinked from '{groupName}'.",
() => () =>
{ {
@@ -395,11 +416,12 @@ public static class ClaimLinkChatCommand
return Stage( return Stage(
playerUid, playerUid,
group.Uid,
$"All of {target.PlayerName}'s claims will be unlinked from '{groupName}'.", $"All of {target.PlayerName}'s claims will be unlinked from '{groupName}'.",
() => () =>
{ {
ClaimLinkModSystem.Registry.RemoveAllForPlayerInGroup(targetUid, group.Uid); ClaimLinkModSystem.Registry.RemoveAllForPlayerInGroup(targetUid, group.Uid);
ClaimLinkCommandListener.PendingLoads.Remove(targetUid); CommandListener.PendingLoads.Remove(targetUid);
PendingActions.Remove(targetUid); PendingActions.Remove(targetUid);
return TextCommandResult.Success( return TextCommandResult.Success(
@@ -429,12 +451,11 @@ public static class ClaimLinkChatCommand
int groupId = group.Uid; int groupId = group.Uid;
return Stage( return Stage(
playerUid, playerUid,
groupId,
$"'{groupName}' will be deleted as a claim link.", $"'{groupName}' will be deleted as a claim link.",
() => () =>
{ {
foreach (string uid in ClaimLinkModSystem.Registry.MemberUidsForGroup(groupId)) RemovePendingActionsForGroup(groupId);
PendingActions.Remove(uid);
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.");
} }
@@ -476,6 +497,7 @@ public static class ClaimLinkChatCommand
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)
); );
@@ -572,16 +594,31 @@ public static class ClaimLinkChatCommand
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 ({groupIds.Count}):"); int shown = 0;
foreach (int groupId in groupIds) foreach (int groupId in groupIds)
{ {
string groupName = ClaimLinkModSystem.Groups.PlayerGroupsById[groupId].Name; if (
!ClaimLinkModSystem.Groups.PlayerGroupsById.TryGetValue(
groupId,
out PlayerGroup? group
)
)
{
RemovePendingActionsForGroup(groupId);
ClaimLinkModSystem.Registry.Remove(groupId);
continue;
}
int memberCount = ClaimLinkModSystem.Registry.MemberCountForGroup(groupId); int memberCount = ClaimLinkModSystem.Registry.MemberCountForGroup(groupId);
sb.AppendLine($" {groupName}: {memberCount} member{(memberCount == 1 ? "" : "s")}"); 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 claimIndex) private static string DescribeClaim(string ownerPlayerUid, int claimIndex)
+9 -3
View File
@@ -13,7 +13,8 @@ public class ClaimLinkModSystem : ModSystem
internal static IGroupManager Groups = null!; internal static IGroupManager Groups = null!;
internal static IWorldAccessor World = null!; internal static IWorldAccessor World = null!;
internal static IPlayerDataManager PlayerData = null!; internal static IPlayerDataManager PlayerData = null!;
internal ClaimLinkCommandListener? CmdListener; internal CommandListener? CmdListener;
internal GroupCommandListener? GroupCmdListener;
public override bool ShouldLoad(EnumAppSide forSide) => forSide == EnumAppSide.Server; public override bool ShouldLoad(EnumAppSide forSide) => forSide == EnumAppSide.Server;
@@ -26,11 +27,14 @@ public class ClaimLinkModSystem : ModSystem
PlayerData = api.PlayerData; PlayerData = api.PlayerData;
Registry = new ClaimLinkRegistry(api.WorldManager.SaveGame); 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);
api.Event.PlayerDisconnect += ClaimLinkCommandListener.OnPlayerDisconnect; api.Event.PlayerDisconnect += CommandListener.OnPlayerDisconnect;
api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect;
api.Event.PlayerDisconnect += ClaimLinkChatCommand.OnPlayerDisconnect; api.Event.PlayerDisconnect += ClaimLinkChatCommand.OnPlayerDisconnect;
} }
@@ -38,6 +42,8 @@ public class ClaimLinkModSystem : ModSystem
{ {
if (CmdListener != null) if (CmdListener != null)
CommandHookModSystem.Unregister(CmdListener); CommandHookModSystem.Unregister(CmdListener);
if (GroupCmdListener != null)
CommandHookModSystem.Unregister(GroupCmdListener);
} }
private static IEnumerable<(int claimIndex, LandClaim claim)> EnumerateOwnedClaims( private static IEnumerable<(int claimIndex, LandClaim claim)> EnumerateOwnedClaims(
+1 -1
View File
@@ -65,7 +65,7 @@ public class ClaimLinkRegistry
public bool AddEntry(string uid, int groupId, int claimIndex) public bool AddEntry(string uid, int groupId, int claimIndex)
{ {
if (ClaimLinkCommandListener.PendingLoads.ContainsKey(uid)) if (CommandListener.PendingLoads.ContainsKey(uid))
return false; return false;
if (!Exists(groupId)) if (!Exists(groupId))
return false; return false;
+63 -51
View File
@@ -5,16 +5,15 @@ using Vintagestory.API.Server;
namespace ClaimLink; namespace ClaimLink;
public class ClaimLinkCommandListener : ICommandHookListener public class CommandListener : ICommandHookListener
{ {
public string ModId => "claimlink"; public string ModId => "claimlink";
public IReadOnlyList<string> Commands => new[] { "land", "group" }; public IReadOnlyList<string> Commands => new[] { "land" };
public CommandRegistration Registration => new(Before, After); public CommandRegistration Registration => new(Before, After);
internal static Dictionary<string, int> PendingLoads = new Dictionary<string, int>(); internal static Dictionary<string, int> PendingLoads = new Dictionary<string, int>();
internal static Dictionary<string, int> PendingDisbands = new Dictionary<string, int>();
private delegate void SubHandler(Caller caller, CmdArgs args); private delegate void SubHandler(Caller caller, CmdArgs args);
@@ -33,23 +32,15 @@ public class ClaimLinkCommandListener : ICommandHookListener
["cancel"] = ClaimCancel, ["cancel"] = ClaimCancel,
}; };
private static readonly Dictionary<string, SubHandler> groupTopLevel = new()
{
["confirmdisband"] = GroupConfirmDisband,
};
private static void Dispatch(TextCommandCallingArgs args) private static void Dispatch(TextCommandCallingArgs args)
{ {
Dictionary<string, SubHandler> table =
args.Command?.Name == "group" ? groupTopLevel : topLevel;
CmdArgs rawArgs = args.RawArgs.Clone(); CmdArgs rawArgs = args.RawArgs.Clone();
while (rawArgs.Length > 0) while (rawArgs.Length > 0)
{ {
string word = rawArgs.PeekWord(); string word = rawArgs.PeekWord();
if (table.TryGetValue(word, out SubHandler? handler)) if (topLevel.TryGetValue(word, out SubHandler? handler))
{ {
rawArgs.PopWord(); rawArgs.PopWord();
handler(args.Caller, rawArgs); handler(args.Caller, rawArgs);
@@ -67,45 +58,7 @@ public class ClaimLinkCommandListener : ICommandHookListener
return null; return null;
} }
private void After(TextCommandCallingArgs args, TextCommandResult result) private void After(TextCommandCallingArgs args, TextCommandResult result) { }
{
if (args.Caller.Player == null)
return;
string uid = args.Caller.Player.PlayerUID;
if (!PendingDisbands.TryGetValue(uid, out int groupId))
return;
PendingDisbands.Remove(uid);
if (ClaimLinkModSystem.Groups.PlayerGroupsById.ContainsKey(groupId))
return;
if (!ClaimLinkModSystem.Registry.Exists(groupId))
return;
foreach (string memberUid in ClaimLinkModSystem.Registry.MemberUidsForGroup(groupId))
ClaimLinkChatCommand.PendingActions.Remove(memberUid);
ClaimLinkModSystem.Registry.Remove(groupId);
}
private static void GroupConfirmDisband(Caller caller, CmdArgs args)
{
if (caller.Player == null)
return;
string? name = args.PopWord();
int? groupId =
name != null
? ClaimLinkModSystem.Groups.GetPlayerGroupByName(name)?.Uid
: caller.FromChatGroupId;
if (groupId == null)
return;
PendingDisbands[caller.Player.PlayerUID] = (int)groupId;
}
private static void Claim(Caller caller, CmdArgs args) private static void Claim(Caller caller, CmdArgs args)
{ {
@@ -210,3 +163,62 @@ public class ClaimLinkCommandListener : ICommandHookListener
PendingLoads.Remove(player.PlayerUID); PendingLoads.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>();
private TextCommandResult? Before(TextCommandCallingArgs args)
{
if (args.Caller.Player == null)
return null;
CmdArgs rawArgs = args.RawArgs.Clone();
if (rawArgs.PopWord() != "confirmdisband")
return null;
string? name = rawArgs.PopWord();
int? groupId =
name != null
? ClaimLinkModSystem.Groups.GetPlayerGroupByName(name)?.Uid
: args.Caller.FromChatGroupId;
if (groupId == null)
return null;
PendingDisbands[args.Caller.Player.PlayerUID] = (int)groupId;
return null;
}
private void After(TextCommandCallingArgs args, TextCommandResult result)
{
if (args.Caller.Player == null)
return;
string uid = args.Caller.Player.PlayerUID;
if (!PendingDisbands.TryGetValue(uid, out int groupId))
return;
PendingDisbands.Remove(uid);
if (ClaimLinkModSystem.Groups.PlayerGroupsById.ContainsKey(groupId))
return;
if (!ClaimLinkModSystem.Registry.Exists(groupId))
return;
ClaimLinkChatCommand.RemovePendingActionsForGroup(groupId);
ClaimLinkModSystem.Registry.Remove(groupId);
}
internal static void OnPlayerDisconnect(IServerPlayer player)
{
PendingDisbands.Remove(player.PlayerUID);
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"description": "Link claims together with groups.", "description": "Link claims together with groups.",
"version": "0.0.6", "version": "0.0.6",
"dependencies": { "dependencies": {
"game": "1.22.3", "game": "1.22.5",
"commandhook": "2.1.0" "commandhook": "2.1.0"
} }
} }