fix: normalize claim geometry before growing buffer (raw start/end marks aren't min/max ordered)

This commit is contained in:
2026-07-27 07:40:12 +02:00
parent baf3ccbd93
commit c110ee2604
3 changed files with 217 additions and 23 deletions
+89 -13
View File
@@ -23,10 +23,7 @@ internal static class ClaimLinkBuffer
continue; continue;
foreach (Cuboidi area in claim.Areas) foreach (Cuboidi area in claim.Areas)
{ cuboids.Add(GrowOne(area));
int bufferSize = ClaimLinkModSystem.Config.BufferSize;
cuboids.Add(area.Clone().GrowBy(bufferSize, bufferSize, bufferSize));
}
} }
} }
@@ -35,23 +32,18 @@ internal static class ClaimLinkBuffer
internal static bool TryFindOverlappingClaimLink( internal static bool TryFindOverlappingClaimLink(
List<Cuboidi> areas, List<Cuboidi> areas,
int? excludeGroupId, ISet<int>? excludeGroupIds,
out int violatingGroupId out int violatingGroupId
) )
{ {
int bufferSize = ClaimLinkModSystem.Config.BufferSize;
List<Cuboidi> grownAreas = new();
foreach (Cuboidi area in areas)
grownAreas.Add(area.Clone().GrowBy(bufferSize, bufferSize, bufferSize));
foreach (int groupId in ClaimLinkModSystem.Registry.All) foreach (int groupId in ClaimLinkModSystem.Registry.All)
{ {
if (groupId == excludeGroupId) if (excludeGroupIds != null && excludeGroupIds.Contains(groupId))
continue; continue;
foreach (Cuboidi bufferCuboid in GetBuffer(groupId)) foreach (Cuboidi bufferCuboid in GetBuffer(groupId))
foreach (Cuboidi grownArea in grownAreas) foreach (Cuboidi area in areas)
if (grownArea.Intersects(bufferCuboid)) if (area.Intersects(bufferCuboid))
{ {
violatingGroupId = groupId; violatingGroupId = groupId;
return true; return true;
@@ -61,4 +53,88 @@ internal static class ClaimLinkBuffer
violatingGroupId = -1; violatingGroupId = -1;
return false; 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);
}
} }
+27 -9
View File
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using Vintagestory.API.Common; using Vintagestory.API.Common;
using Vintagestory.API.Config; using Vintagestory.API.Config;
using Vintagestory.API.MathTools;
using Vintagestory.API.Server; using Vintagestory.API.Server;
namespace ClaimLink; namespace ClaimLink;
@@ -379,15 +380,18 @@ public static class ClaimLinkChatCommand
!ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out LandClaim? claim) !ClaimLinkModSystem.TryResolveOwnedClaim(playerUid, claimIndex, out LandClaim? claim)
|| claim == null || 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)) 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 ( if (
ClaimLinkBuffer.TryFindOverlappingClaimLink( ClaimLinkBuffer.TryFindOverlappingClaimLink(
claim.Areas, grownAreas,
groupId, excludeGroupIds,
out int violatingGroupId 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); string claimDesc = DescribeClaim(playerUid, claimIndex);
return Stage( return Stage(
playerUid, playerUid,
groupId, groupId,
$"{claimDesc} will be linked into '{groupName}'.", $"'{claimDesc}' will be linked into '{groupName}'.",
() => () =>
{ {
bool stillMember = bool stillMember =
@@ -419,9 +437,9 @@ public static class ClaimLinkChatCommand
bool success = ClaimLinkModSystem.Registry.AddEntry(playerUid, groupId, claimIndex); bool success = ClaimLinkModSystem.Registry.AddEntry(playerUid, groupId, claimIndex);
return success return success
? TextCommandResult.Success($"Linked {claimDesc} to '{groupName}'.") ? TextCommandResult.Success($"Linked '{claimDesc}' to '{groupName}'.")
: TextCommandResult.Error( : 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( return Stage(
playerUid, playerUid,
(int)groupId, (int)groupId,
$"{claimDesc} will be unlinked from '{groupName}'.", $"'{claimDesc}' will be unlinked from '{groupName}'.",
() => () =>
{ {
ClaimLinkModSystem.Registry.RemoveEntry(playerUid, (int)groupId, claimIndex); 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); ClaimLinkModSystem.Registry.RemoveEntry(targetUid, (int)groupId, claimIndex);
return TextCommandResult.Success( return TextCommandResult.Success(
$"Unlinked {claimDesc} from '{groupName}' (owned by {playerName})." $"Unlinked '{claimDesc}' from '{groupName}' (owned by {playerName})."
); );
} }
+101 -1
View File
@@ -1,6 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using CommandHook; using CommandHook;
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;
@@ -14,6 +16,8 @@ public class CommandListener : ICommandHookListener
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, List<Cuboidi>?> PendingSaveChecks =
new Dictionary<string, List<Cuboidi>?>();
private delegate void SubHandler(Caller caller, CmdArgs args); private delegate void SubHandler(Caller caller, CmdArgs args);
@@ -59,7 +63,89 @@ public class CommandListener : 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 (!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) private static void Claim(Caller caller, CmdArgs args)
{ {
@@ -80,9 +166,22 @@ public class CommandListener : ICommandHookListener
return; return;
string uid = caller.Player.PlayerUID; string uid = caller.Player.PlayerUID;
PendingSaveChecks[uid] = null;
if (!PendingLoads.TryGetValue(uid, out int pendingIndex)) if (!PendingLoads.TryGetValue(uid, out int pendingIndex))
return; 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); ClaimLinkModSystem.Registry.ClaimSaved(uid, pendingIndex);
PendingLoads.Remove(uid); PendingLoads.Remove(uid);
ClaimLinkChatCommand.PendingActions.Remove(uid); ClaimLinkChatCommand.PendingActions.Remove(uid);
@@ -170,6 +269,7 @@ public class CommandListener : ICommandHookListener
internal static void OnPlayerDisconnect(IServerPlayer player) internal static void OnPlayerDisconnect(IServerPlayer player)
{ {
PendingLoads.Remove(player.PlayerUID); PendingLoads.Remove(player.PlayerUID);
PendingSaveChecks.Remove(player.PlayerUID);
} }
} }