From 62c94bc7752740e2261f02d1526015dcc5c788c0 Mon Sep 17 00:00:00 2001 From: anth64 Date: Tue, 25 Aug 2026 23:56:57 +0200 Subject: [PATCH] feat: push-based buffer/permission sync for the client-side TestBlockAccess pre-check The server now pushes buffer and permission state to clients continuously: a full sync when a player joins, and an incremental delta on every registry mutation, mirroring vanilla's own land claim sync model. ClaimLinkBufferIndex is now instantiable, so the client and server each get their own region-indexed lookup built from the same algorithm. --- ClaimLink/ClaimLinkBufferIndex.cs | 37 ++++--- ClaimLink/ClaimLinkBufferProtection.cs | 4 +- ClaimLink/ClaimLinkModSystem.cs | 14 ++- ClaimLink/ClaimLinkRegistry.cs | 47 +++++---- ClaimLink/ClaimLinkVisualizerClient.cs | 132 +++++++++++++++++++++--- ClaimLink/ClaimLinkVisualizerNetwork.cs | 101 ++++++++++-------- ClaimLink/CommandListener.cs | 2 +- 7 files changed, 240 insertions(+), 97 deletions(-) diff --git a/ClaimLink/ClaimLinkBufferIndex.cs b/ClaimLink/ClaimLinkBufferIndex.cs index 5f56d15..dbcd063 100644 --- a/ClaimLink/ClaimLinkBufferIndex.cs +++ b/ClaimLink/ClaimLinkBufferIndex.cs @@ -1,25 +1,32 @@ +using System; using System.Collections.Generic; using Vintagestory.API.MathTools; namespace ClaimLink; -internal static class ClaimLinkBufferIndex +internal class ClaimLinkBufferIndex { - private static readonly Dictionary> groupIdsByRegion = new(); - private static readonly Dictionary> buffersByGroupId = new(); + private readonly Func regionIndex; + private readonly Dictionary> groupIdsByRegion = new(); + private readonly Dictionary> buffersByGroupId = new(); + private int regionSize; - internal static void Rebuild() + internal ClaimLinkBufferIndex(Func regionIndex) { + this.regionIndex = regionIndex; + } + + internal void Rebuild(IEnumerable<(int groupId, List buffers)> source, int regionSize) + { + this.regionSize = regionSize; groupIdsByRegion.Clear(); buffersByGroupId.Clear(); - int regionSize = ClaimLinkModSystem.WorldManager.RegionSize; if (regionSize <= 0) return; - foreach (int groupId in ClaimLinkModSystem.Registry.All) + foreach ((int groupId, List buffers) in source) { - List buffers = ClaimLinkBuffer.GetBuffer(groupId); buffersByGroupId[groupId] = buffers; HashSet regionKeys = new(); @@ -32,7 +39,7 @@ internal static class ClaimLinkBufferIndex for (int rx = minRegionX; rx <= maxRegionX; rx++) for (int rz = minRegionZ; rz <= maxRegionZ; rz++) - regionKeys.Add(ClaimLinkModSystem.WorldManager.MapRegionIndex2D(rx, rz)); + regionKeys.Add(regionIndex(rx, rz)); } foreach (long regionKey in regionKeys) @@ -44,23 +51,19 @@ internal static class ClaimLinkBufferIndex } } - internal static IEnumerable CandidateGroupIds(BlockPos pos) + internal IEnumerable CandidateGroupIds(BlockPos pos) { - int regionSize = ClaimLinkModSystem.WorldManager.RegionSize; if (regionSize <= 0) - return System.Array.Empty(); + return Array.Empty(); - long regionKey = ClaimLinkModSystem.WorldManager.MapRegionIndex2D( - pos.X / regionSize, - pos.Z / regionSize - ); + long regionKey = regionIndex(pos.X / regionSize, pos.Z / regionSize); return groupIdsByRegion.TryGetValue(regionKey, out HashSet? groupIds) ? groupIds - : System.Array.Empty(); + : Array.Empty(); } - internal static List CachedBuffer(int groupId) => + internal List CachedBuffer(int groupId) => buffersByGroupId.TryGetValue(groupId, out List? buffers) ? buffers : new List(); diff --git a/ClaimLink/ClaimLinkBufferProtection.cs b/ClaimLink/ClaimLinkBufferProtection.cs index ede7d99..11b199e 100644 --- a/ClaimLink/ClaimLinkBufferProtection.cs +++ b/ClaimLink/ClaimLinkBufferProtection.cs @@ -34,12 +34,12 @@ internal static class ClaimLinkBufferProtection { bool allowed = true; - foreach (int groupId in ClaimLinkBufferIndex.CandidateGroupIds(pos)) + foreach (int groupId in ClaimLinkModSystem.BufferIndex.CandidateGroupIds(pos)) { if (IsMember(byPlayer, groupId)) continue; - foreach (Cuboidi buffer in ClaimLinkBufferIndex.CachedBuffer(groupId)) + foreach (Cuboidi buffer in ClaimLinkModSystem.BufferIndex.CachedBuffer(groupId)) if ( buffer.Contains(pos) && !ClaimLinkModSystem.Registry.HasPermission(groupId, byPlayer, accessFlag) diff --git a/ClaimLink/ClaimLinkModSystem.cs b/ClaimLink/ClaimLinkModSystem.cs index 8e6f965..2a24b1d 100644 --- a/ClaimLink/ClaimLinkModSystem.cs +++ b/ClaimLink/ClaimLinkModSystem.cs @@ -15,6 +15,7 @@ public class ClaimLinkModSystem : ModSystem internal static IWorldManagerAPI WorldManager = null!; internal static IPlayerDataManager PlayerData = null!; internal static ClaimLinkConfig Config = null!; + internal static ClaimLinkBufferIndex BufferIndex = null!; private object? cmdListenerProxy; private object? groupCmdListenerProxy; @@ -29,8 +30,9 @@ public class ClaimLinkModSystem : ModSystem Config = api.LoadModConfig("claimlink.json") ?? new ClaimLinkConfig(); api.StoreModConfig(Config, "claimlink.json"); Registry = new ClaimLinkRegistry(api.WorldManager.SaveGame); - ClaimLinkBufferIndex.Rebuild(); - api.Event.SaveGameLoaded += ClaimLinkBufferIndex.Rebuild; + BufferIndex = new ClaimLinkBufferIndex(WorldManager.MapRegionIndex2D); + RebuildBufferIndex(); + api.Event.SaveGameLoaded += RebuildBufferIndex; if (api.ModLoader.IsModEnabled("commandhook")) { @@ -75,6 +77,14 @@ public class ClaimLinkModSystem : ModSystem api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect; } + internal static void RebuildBufferIndex() + { + List<(int groupId, List buffers)> source = new(); + foreach (int groupId in Registry.All) + source.Add((groupId, ClaimLinkBuffer.GetBuffer(groupId))); + BufferIndex.Rebuild(source, WorldManager.RegionSize); + } + public override void StartClientSide(ICoreClientAPI api) => ClaimLinkVisualizerClient.Start(api); public override void Dispose() diff --git a/ClaimLink/ClaimLinkRegistry.cs b/ClaimLink/ClaimLinkRegistry.cs index 2319900..f13cbad 100644 --- a/ClaimLink/ClaimLinkRegistry.cs +++ b/ClaimLink/ClaimLinkRegistry.cs @@ -58,7 +58,7 @@ public class ClaimLinkRegistry public void Add(int groupId) { claimLinks.Add(groupId); - Save(); + Save(new[] { groupId }); } public void Remove(int groupId) @@ -69,7 +69,7 @@ public class ClaimLinkRegistry permittedPlayerUids.Remove(groupId); permittedPlayerGroupIds.Remove(groupId); Prune(); - Save(); + Save(new[] { groupId }); } public void GrantPlayer(int groupId, string uid, EnumBlockAccessFlags flags) @@ -77,7 +77,7 @@ public class ClaimLinkRegistry if (!permittedPlayerUids.TryGetValue(groupId, out var uids)) permittedPlayerUids[groupId] = uids = new(); uids[uid] = flags; - Save(); + Save(new[] { groupId }); } public bool RevokePlayer(int groupId, string uid) @@ -87,7 +87,7 @@ public class ClaimLinkRegistry return false; if (uids!.Count == 0) permittedPlayerUids.Remove(groupId); - Save(); + Save(new[] { groupId }); return true; } @@ -96,7 +96,7 @@ public class ClaimLinkRegistry if (!permittedPlayerGroupIds.TryGetValue(groupId, out var groups)) permittedPlayerGroupIds[groupId] = groups = new(); groups[targetGroupId] = flags; - Save(); + Save(new[] { groupId }); } public bool RevokeGroup(int groupId, int targetGroupId) @@ -108,7 +108,7 @@ public class ClaimLinkRegistry return false; if (groups!.Count == 0) permittedPlayerGroupIds.Remove(groupId); - Save(); + Save(new[] { groupId }); return true; } @@ -138,12 +138,12 @@ public class ClaimLinkRegistry public void RemoveGroupFromAllPermissions(int targetGroupId) { - bool changed = false; - foreach (var groups in permittedPlayerGroupIds.Values) + List? changedGroupIds = null; + foreach (var (groupId, groups) in permittedPlayerGroupIds) if (groups.Remove(targetGroupId)) - changed = true; + (changedGroupIds ??= new()).Add(groupId); - if (!changed) + if (changedGroupIds == null) return; List? emptyGroupIds = null; @@ -154,7 +154,7 @@ public class ClaimLinkRegistry foreach (int groupId in emptyGroupIds) permittedPlayerGroupIds.Remove(groupId); - Save(); + Save(changedGroupIds); } public bool IsClaimLinked(string uid, int claimIndex) => @@ -181,7 +181,7 @@ public class ClaimLinkRegistry if (!groups.TryGetValue(groupId, out var claims)) groups[groupId] = claims = new(); claims.Add(claimIndex); - Save(); + Save(new[] { groupId }); return true; } @@ -190,7 +190,7 @@ public class ClaimLinkRegistry if (entries.TryGetValue(uid, out var groups) && groups.TryGetValue(groupId, out var claims)) claims.Remove(claimIndex); Prune(); - Save(); + Save(new[] { groupId }); } public void ClaimRemoved(string uid, int removedIndex) @@ -198,6 +198,8 @@ public class ClaimLinkRegistry if (!entries.TryGetValue(uid, out var groups)) return; + int[] affectedGroupIds = new List(groups.Keys).ToArray(); + foreach (var claims in groups.Values) { claims.Remove(removedIndex); @@ -207,7 +209,7 @@ public class ClaimLinkRegistry } Prune(); - Save(); + Save(affectedGroupIds); } public void ClaimSaved(string uid, int claimIndex) @@ -215,6 +217,8 @@ public class ClaimLinkRegistry if (!entries.TryGetValue(uid, out var groups)) return; + int[] affectedGroupIds = new List(groups.Keys).ToArray(); + int count = 0; foreach (Vintagestory.API.Common.LandClaim c in ClaimLinkModSystem.LandClaimAPI.All) if (c.OwnedByPlayerUid == uid) @@ -233,7 +237,7 @@ public class ClaimLinkRegistry claims[i] = newIndex; } - Save(); + Save(affectedGroupIds); } public bool HasAnyEntry(string uid, int groupId) => @@ -246,13 +250,17 @@ public class ClaimLinkRegistry if (entries.TryGetValue(uid, out var groups)) groups.Remove(groupId); Prune(); - Save(); + Save(new[] { groupId }); } public void ClearAllForPlayer(string uid) { + int[] affectedGroupIds = entries.TryGetValue(uid, out var groups) + ? new List(groups.Keys).ToArray() + : System.Array.Empty(); + entries.Remove(uid); - Save(); + Save(affectedGroupIds); } public IEnumerable MemberUidsForGroup(int groupId) @@ -286,7 +294,7 @@ public class ClaimLinkRegistry entries.Remove(uid); } - public void Save() + private void Save(IReadOnlyCollection affectedGroupIds) { var entryList = new List(); foreach (var (uid, groups) in entries) @@ -331,6 +339,7 @@ public class ClaimLinkRegistry } ); - ClaimLinkBufferIndex.Rebuild(); + ClaimLinkModSystem.RebuildBufferIndex(); + ClaimLinkVisualizerNetwork.BroadcastDelta(affectedGroupIds); } } diff --git a/ClaimLink/ClaimLinkVisualizerClient.cs b/ClaimLink/ClaimLinkVisualizerClient.cs index aa71c8b..e8a4bc7 100644 --- a/ClaimLink/ClaimLinkVisualizerClient.cs +++ b/ClaimLink/ClaimLinkVisualizerClient.cs @@ -8,10 +8,14 @@ namespace ClaimLink; internal static class ClaimLinkVisualizerClient { private const int HighlightSlotId = 8341; + private const int ClientRegionSize = 512; + private static ICoreClientAPI capi = null!; private static bool visible; private static ClaimLinkVisualizerConfig config = null!; private static IClientNetworkChannel channel = null!; + private static readonly Dictionary knownEntries = new(); + private static readonly ClaimLinkBufferIndex bufferIndex = new(ClientRegionKey); internal static void Start(ICoreClientAPI api) { @@ -20,9 +24,8 @@ internal static class ClaimLinkVisualizerClient api.StoreModConfig(config, "claimlinkvisualizer.json"); channel = api.Network.RegisterChannel(ClaimLinkVisualizerNetwork.ChannelName) - .RegisterMessageType() .RegisterMessageType() - .SetMessageHandler(OnBufferResponse); + .SetMessageHandler(OnBuffersPushed); api.Input.RegisterHotKey( "claimlinkvisualizertoggle", @@ -32,35 +35,134 @@ internal static class ClaimLinkVisualizerClient ctrlPressed: true ); api.Input.SetHotKeyHandler("claimlinkvisualizertoggle", OnToggle); + + api.Event.TestBlockAccess += OnTestBlockAccess; } + private static long ClientRegionKey(int regionX, int regionZ) => ((long)regionX << 32) | (uint)regionZ; + private static bool OnToggle(KeyCombination comb) { visible = !visible; if (visible) - { - if (channel.Connected) - channel.SendPacket(new ClaimLinkBufferRequest()); - } + RenderHighlights(); else - { Clear(); - } return true; } - private static void OnBufferResponse(ClaimLinkBufferResponse response) + private static void OnBuffersPushed(ClaimLinkBufferResponse response) { - if (!visible) - return; - - List positions = new(); - List colors = new(); + if (response.IsFullSync) + knownEntries.Clear(); foreach (ClaimLinkBufferEntryDto entry in response.Entries) { - int color = HashToColor(entry.GroupId); + if (entry.Cuboids.Count == 0) + knownEntries.Remove(entry.GroupId); + else + knownEntries[entry.GroupId] = entry; + } + + RebuildIndex(); + + if (visible) + RenderHighlights(); + } + + private static void RebuildIndex() + { + List<(int groupId, List buffers)> source = new(); + + foreach ((int groupId, ClaimLinkBufferEntryDto entry) in knownEntries) + { + List cuboids = new(); + foreach (ClaimLinkBufferCuboidDto c in entry.Cuboids) + cuboids.Add(new Cuboidi(c.MinX, c.MinY, c.MinZ, c.MaxX, c.MaxY, c.MaxZ)); + source.Add((groupId, cuboids)); + } + + bufferIndex.Rebuild(source, ClientRegionSize); + } + + private static EnumWorldAccessResponse OnTestBlockAccess( + IPlayer player, + BlockSelection blockSel, + EnumBlockAccessFlags accessType, + ref string claimant, + EnumWorldAccessResponse response + ) + { + if (response != EnumWorldAccessResponse.Granted) + return response; + + BlockPos pos = blockSel.Position; + + foreach (int groupId in bufferIndex.CandidateGroupIds(pos)) + { + if (IsMember(groupId)) + continue; + + bool inBuffer = false; + foreach (Cuboidi buffer in bufferIndex.CachedBuffer(groupId)) + if (buffer.Contains(pos)) + { + inBuffer = true; + break; + } + if (!inBuffer) + continue; + + EnumBlockAccessFlags granted = knownEntries.TryGetValue(groupId, out var entry) + ? (EnumBlockAccessFlags)entry.GrantedAccess + : EnumBlockAccessFlags.None; + if ((granted & accessType) != 0) + continue; + + if (HasClaimAccess(pos, accessType)) + continue; + + claimant = "the claim link's buffer zone"; + return EnumWorldAccessResponse.DeniedByMod; + } + + return response; + } + + private static bool IsMember(int groupId) + { + foreach (PlayerGroupMembership membership in capi.World.Player.Groups) + if (membership.GroupUid == groupId) + return true; + return false; + } + + private static bool HasClaimAccess(BlockPos pos, EnumBlockAccessFlags accessFlag) + { + LandClaim[]? claims = capi.World.Claims.Get(pos); + if (claims == null) + return false; + + foreach (LandClaim claim in claims) + { + if (claim.TestPlayerAccess(capi.World.Player, accessFlag) != EnumPlayerAccessResult.Denied) + return true; + if (accessFlag == EnumBlockAccessFlags.Use && claim.AllowUseEveryone) + return true; + } + + return false; + } + + private static void RenderHighlights() + { + List positions = new(); + List colors = new(); + + foreach ((int groupId, ClaimLinkBufferEntryDto entry) in knownEntries) + { + int color = HashToColor(groupId); foreach (ClaimLinkBufferCuboidDto cuboid in entry.Cuboids) { positions.Add(new BlockPos(cuboid.MinX, cuboid.MinY, cuboid.MinZ)); diff --git a/ClaimLink/ClaimLinkVisualizerNetwork.cs b/ClaimLink/ClaimLinkVisualizerNetwork.cs index a6dd693..2959c3d 100644 --- a/ClaimLink/ClaimLinkVisualizerNetwork.cs +++ b/ClaimLink/ClaimLinkVisualizerNetwork.cs @@ -1,13 +1,11 @@ using System.Collections.Generic; using ProtoBuf; +using Vintagestory.API.Common; using Vintagestory.API.MathTools; using Vintagestory.API.Server; namespace ClaimLink; -[ProtoContract] -public class ClaimLinkBufferRequest { } - [ProtoContract] public class ClaimLinkBufferCuboidDto { @@ -38,6 +36,9 @@ public class ClaimLinkBufferEntryDto [ProtoMember(2)] public List Cuboids = new(); + + [ProtoMember(3)] + public int GrantedAccess; } [ProtoContract] @@ -45,62 +46,80 @@ public class ClaimLinkBufferResponse { [ProtoMember(1)] public List Entries = new(); + + [ProtoMember(2)] + public bool IsFullSync; } internal static class ClaimLinkVisualizerNetwork { internal const string ChannelName = "claimlinkvisualizer"; + private static IServerNetworkChannel channel = null!; internal static void Start(ICoreServerAPI api) { - api.Network.RegisterChannel(ChannelName) - .RegisterMessageType() - .RegisterMessageType() - .SetMessageHandler( - (fromPlayer, _) => SendBuffers(api, fromPlayer) - ); + channel = api.Network.RegisterChannel(ChannelName).RegisterMessageType(); + + api.Event.PlayerNowPlaying += SendFullSync; } - private static void SendBuffers(ICoreServerAPI api, IServerPlayer toPlayer) + private static void SendFullSync(IServerPlayer toPlayer) { - ClaimLinkBufferResponse response = new(); - - BlockPos playerPos = toPlayer.Entity.Pos.AsBlockPos; - int radius = toPlayer.WorldData.LastApprovedViewDistance; + ClaimLinkBufferResponse response = new() { IsFullSync = true }; 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, - } - ); - } - + ClaimLinkBufferEntryDto entry = BuildEntry(groupId, toPlayer); if (entry.Cuboids.Count > 0) response.Entries.Add(entry); } - api.Network.GetChannel(ChannelName).SendPacket(response, toPlayer); + channel.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; + internal static void BroadcastDelta(IReadOnlyCollection affectedGroupIds) + { + if (affectedGroupIds.Count == 0 || channel == null) + return; + + foreach (IPlayer onlinePlayer in ClaimLinkModSystem.World.AllOnlinePlayers) + { + if (onlinePlayer is not IServerPlayer serverPlayer) + continue; + + ClaimLinkBufferResponse response = new() { IsFullSync = false }; + foreach (int groupId in affectedGroupIds) + response.Entries.Add(BuildEntry(groupId, serverPlayer)); + + channel.SendPacket(response, serverPlayer); + } + } + + private static ClaimLinkBufferEntryDto BuildEntry(int groupId, IServerPlayer forPlayer) + { + EnumBlockAccessFlags granted = EnumBlockAccessFlags.None; + if (ClaimLinkModSystem.Registry.HasPermission(groupId, forPlayer, EnumBlockAccessFlags.BuildOrBreak)) + granted |= EnumBlockAccessFlags.BuildOrBreak; + if (ClaimLinkModSystem.Registry.HasPermission(groupId, forPlayer, EnumBlockAccessFlags.Use)) + granted |= EnumBlockAccessFlags.Use; + + ClaimLinkBufferEntryDto entry = new() { GroupId = groupId, GrantedAccess = (int)granted }; + + foreach (Cuboidi cuboid in ClaimLinkBuffer.GetBuffer(groupId)) + { + entry.Cuboids.Add( + new ClaimLinkBufferCuboidDto + { + MinX = cuboid.MinX, + MinY = cuboid.MinY, + MinZ = cuboid.MinZ, + MaxX = cuboid.MaxX, + MaxY = cuboid.MaxY, + MaxZ = cuboid.MaxZ, + } + ); + } + + return entry; + } } diff --git a/ClaimLink/CommandListener.cs b/ClaimLink/CommandListener.cs index 07d1944..58f64af 100644 --- a/ClaimLink/CommandListener.cs +++ b/ClaimLink/CommandListener.cs @@ -111,7 +111,7 @@ public class CommandListener if (!violatesClaimLink && !violatesClaim) { - ClaimLinkBufferIndex.Rebuild(); + ClaimLinkModSystem.RebuildBufferIndex(); return; }