From c110ee26047f6d54c909aca9e59d1804335818eb Mon Sep 17 00:00:00 2001 From: anth64 Date: Mon, 27 Jul 2026 07:40:12 +0200 Subject: [PATCH] fix: normalize claim geometry before growing buffer (raw start/end marks aren't min/max ordered) --- ClaimLink/ClaimLinkBuffer.cs | 102 ++++++++++++++++++++++++++---- ClaimLink/ClaimLinkChatCommand.cs | 36 ++++++++--- ClaimLink/CommandListener.cs | 102 +++++++++++++++++++++++++++++- 3 files changed, 217 insertions(+), 23 deletions(-) diff --git a/ClaimLink/ClaimLinkBuffer.cs b/ClaimLink/ClaimLinkBuffer.cs index 36fbf98..62af0e2 100644 --- a/ClaimLink/ClaimLinkBuffer.cs +++ b/ClaimLink/ClaimLinkBuffer.cs @@ -23,10 +23,7 @@ internal static class ClaimLinkBuffer continue; foreach (Cuboidi area in claim.Areas) - { - int bufferSize = ClaimLinkModSystem.Config.BufferSize; - cuboids.Add(area.Clone().GrowBy(bufferSize, bufferSize, bufferSize)); - } + cuboids.Add(GrowOne(area)); } } @@ -35,23 +32,18 @@ internal static class ClaimLinkBuffer internal static bool TryFindOverlappingClaimLink( List areas, - int? excludeGroupId, + ISet? excludeGroupIds, out int violatingGroupId ) { - int bufferSize = ClaimLinkModSystem.Config.BufferSize; - List grownAreas = new(); - foreach (Cuboidi area in areas) - grownAreas.Add(area.Clone().GrowBy(bufferSize, bufferSize, bufferSize)); - foreach (int groupId in ClaimLinkModSystem.Registry.All) { - if (groupId == excludeGroupId) + if (excludeGroupIds != null && excludeGroupIds.Contains(groupId)) continue; foreach (Cuboidi bufferCuboid in GetBuffer(groupId)) - foreach (Cuboidi grownArea in grownAreas) - if (grownArea.Intersects(bufferCuboid)) + foreach (Cuboidi area in areas) + if (area.Intersects(bufferCuboid)) { violatingGroupId = groupId; return true; @@ -61,4 +53,88 @@ internal static class ClaimLinkBuffer violatingGroupId = -1; return false; } + + internal static bool TryFindOverlappingClaim( + List areas, + string excludeOwnerUid, + ISet? excludeGroupIds, + out LandClaim? violatingClaim + ) + { + HashSet 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 MemberClaimLinkIds(string uid) + { + HashSet 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 GrowAreas(List areas) + { + List 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); + } } diff --git a/ClaimLink/ClaimLinkChatCommand.cs b/ClaimLink/ClaimLinkChatCommand.cs index 2d394d1..20ea680 100644 --- a/ClaimLink/ClaimLinkChatCommand.cs +++ b/ClaimLink/ClaimLinkChatCommand.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using Vintagestory.API.Common; using Vintagestory.API.Config; +using Vintagestory.API.MathTools; using Vintagestory.API.Server; namespace ClaimLink; @@ -379,15 +380,18 @@ public static class ClaimLinkChatCommand !ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out LandClaim? claim) || claim == null ) - return TextCommandResult.Error("You do not own that claim."); + return TextCommandResult.Error($"You don't have a claim at index {claimIndex}."); if (ClaimLinkModSystem.Registry.IsClaimLinked(playerUid, claimIndex)) return TextCommandResult.Error("That claim is already part of a claim link."); + List grownAreas = ClaimLinkBuffer.GrowAreas(claim.Areas); + ISet excludeGroupIds = new HashSet { groupId }; + if ( ClaimLinkBuffer.TryFindOverlappingClaimLink( - claim.Areas, - groupId, + grownAreas, + excludeGroupIds, out int violatingGroupId ) ) @@ -403,11 +407,25 @@ public static class ClaimLinkChatCommand ); } + 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}'.", + $"'{claimDesc}' will be linked into '{groupName}'.", () => { bool stillMember = @@ -419,9 +437,9 @@ public static class ClaimLinkChatCommand bool success = ClaimLinkModSystem.Registry.AddEntry(playerUid, groupId, claimIndex); return success - ? TextCommandResult.Success($"Linked {claimDesc} to '{groupName}'.") + ? TextCommandResult.Success($"Linked '{claimDesc}' to '{groupName}'.") : TextCommandResult.Error( - $"Unable to link {claimIndex} to '{groupName}' it is currently being modified." + $"Unable to link claim {claimIndex} to '{groupName}', it is currently being modified." ); } ); @@ -444,11 +462,11 @@ public static class ClaimLinkChatCommand return Stage( playerUid, (int)groupId, - $"{claimDesc} will be unlinked from '{groupName}'.", + $"'{claimDesc}' will be unlinked from '{groupName}'.", () => { ClaimLinkModSystem.Registry.RemoveEntry(playerUid, (int)groupId, claimIndex); - return TextCommandResult.Success($"Unlinked {claimDesc} from '{groupName}'."); + return TextCommandResult.Success($"Unlinked '{claimDesc}' from '{groupName}'."); } ); } @@ -828,7 +846,7 @@ public static class ClaimLinkChatCommand ClaimLinkModSystem.Registry.RemoveEntry(targetUid, (int)groupId, claimIndex); return TextCommandResult.Success( - $"Unlinked {claimDesc} from '{groupName}' (owned by {playerName})." + $"Unlinked '{claimDesc}' from '{groupName}' (owned by {playerName})." ); } diff --git a/ClaimLink/CommandListener.cs b/ClaimLink/CommandListener.cs index c1132b9..40cd2f0 100644 --- a/ClaimLink/CommandListener.cs +++ b/ClaimLink/CommandListener.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using CommandHook; using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.MathTools; using Vintagestory.API.Server; namespace ClaimLink; @@ -14,6 +16,8 @@ public class CommandListener : ICommandHookListener public CommandRegistration Registration => new(Before, After); internal static Dictionary PendingLoads = new Dictionary(); + internal static Dictionary?> PendingSaveChecks = + new Dictionary?>(); private delegate void SubHandler(Caller caller, CmdArgs args); @@ -59,7 +63,89 @@ public class CommandListener : ICommandHookListener 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 (!PendingSaveChecks.Remove(uid, out List? snapshot)) + return; + + if (result.Status != EnumCommandStatus.Success) + return; + + List 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 checkAreas = isLinked + ? ClaimLinkBuffer.GrowAreas(savedClaim.Areas) + : savedClaim.Areas; + + ISet excludeGroupIds = isLinked + ? new HashSet { 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) { @@ -80,9 +166,22 @@ public class CommandListener : ICommandHookListener 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 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); @@ -170,6 +269,7 @@ public class CommandListener : ICommandHookListener internal static void OnPlayerDisconnect(IServerPlayer player) { PendingLoads.Remove(player.PlayerUID); + PendingSaveChecks.Remove(player.PlayerUID); } }