From b14eb5c538661775716b5e38eadc8564ee57846f Mon Sep 17 00:00:00 2001 From: anth64 Date: Tue, 25 Aug 2026 23:14:58 +0200 Subject: [PATCH] feat: convert to Universal mod, merge ClaimLinkVisualizer in, keep CommandHook as a true soft dependency via runtime proxy --- ClaimLink/ClaimLinkModSystem.cs | 72 ++++++++++++++---- ClaimLink/ClaimLinkVisualizerClient.cs | 101 +++++++++++++++++++++++++ ClaimLink/ClaimLinkVisualizerConfig.cs | 6 ++ ClaimLink/CommandHookProxy.cs | 45 +++++++++++ ClaimLink/CommandListener.cs | 17 ++--- ClaimLink/modinfo.json | 5 +- 6 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 ClaimLink/ClaimLinkVisualizerClient.cs create mode 100644 ClaimLink/ClaimLinkVisualizerConfig.cs create mode 100644 ClaimLink/CommandHookProxy.cs diff --git a/ClaimLink/ClaimLinkModSystem.cs b/ClaimLink/ClaimLinkModSystem.cs index 01a2ff2..8e6f965 100644 --- a/ClaimLink/ClaimLinkModSystem.cs +++ b/ClaimLink/ClaimLinkModSystem.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using CommandHook; +using Vintagestory.API.Client; using Vintagestory.API.Common; using Vintagestory.API.Server; @@ -15,10 +15,8 @@ public class ClaimLinkModSystem : ModSystem internal static IWorldManagerAPI WorldManager = null!; 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; + private object? cmdListenerProxy; + private object? groupCmdListenerProxy; public override void StartServerSide(ICoreServerAPI api) { @@ -34,25 +32,67 @@ public class ClaimLinkModSystem : ModSystem ClaimLinkBufferIndex.Rebuild(); api.Event.SaveGameLoaded += ClaimLinkBufferIndex.Rebuild; - CmdListener = new CommandListener(); - CommandHookModSystem.Register(CmdListener); - GroupCmdListener = new GroupCommandListener(); - CommandHookModSystem.Register(GroupCmdListener); + if (api.ModLoader.IsModEnabled("commandhook")) + { + api.Event.SaveGameLoaded += () => RegisterCommandHooks(api); + } + else + { + api.Logger.Fatal( + "ClaimLink requires the CommandHook mod on the server for /land and /group integration. That integration is disabled; /claimlink commands and buffer protection still work." + ); + } + ClaimLinkChatCommand.Register(api); ClaimLinkVisualizerNetwork.Start(api); ClaimLinkBufferProtection.Register(api); - - api.Event.PlayerDisconnect += CommandListener.OnPlayerDisconnect; - api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect; api.Event.PlayerDisconnect += ClaimLinkChatCommand.OnPlayerDisconnect; } + private void RegisterCommandHooks(ICoreServerAPI api) + { + CommandListener cmdListener = new(); + CommandHook.ICommandHookListener cmdProxy = CommandHookListenerProxy.Create( + cmdListener.ModId, + cmdListener.Commands, + cmdListener.Before, + cmdListener.After + ); + CommandHook.CommandHookModSystem.Register(cmdProxy); + cmdListenerProxy = cmdProxy; + + GroupCommandListener groupListener = new(); + CommandHook.ICommandHookListener groupProxy = CommandHookListenerProxy.Create( + groupListener.ModId, + groupListener.Commands, + groupListener.Before, + groupListener.After + ); + CommandHook.CommandHookModSystem.Register(groupProxy); + groupCmdListenerProxy = groupProxy; + + api.Event.PlayerDisconnect += CommandListener.OnPlayerDisconnect; + api.Event.PlayerDisconnect += GroupCommandListener.OnPlayerDisconnect; + } + + public override void StartClientSide(ICoreClientAPI api) => ClaimLinkVisualizerClient.Start(api); + public override void Dispose() { - if (CmdListener != null) - CommandHookModSystem.Unregister(CmdListener); - if (GroupCmdListener != null) - CommandHookModSystem.Unregister(GroupCmdListener); + if (cmdListenerProxy != null || groupCmdListenerProxy != null) + UnregisterCommandHooks(); + } + + private void UnregisterCommandHooks() + { + if (cmdListenerProxy != null) + CommandHook.CommandHookModSystem.Unregister( + (CommandHook.ICommandHookListener)cmdListenerProxy + ); + if (groupCmdListenerProxy != null) + CommandHook.CommandHookModSystem.Unregister( + (CommandHook.ICommandHookListener)groupCmdListenerProxy + ); } private static IEnumerable<(int claimIndex, LandClaim claim)> EnumerateOwnedClaims( diff --git a/ClaimLink/ClaimLinkVisualizerClient.cs b/ClaimLink/ClaimLinkVisualizerClient.cs new file mode 100644 index 0000000..aa71c8b --- /dev/null +++ b/ClaimLink/ClaimLinkVisualizerClient.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace ClaimLink; + +internal static class ClaimLinkVisualizerClient +{ + private const int HighlightSlotId = 8341; + private static ICoreClientAPI capi = null!; + private static bool visible; + private static ClaimLinkVisualizerConfig config = null!; + private static IClientNetworkChannel channel = null!; + + internal static void Start(ICoreClientAPI api) + { + capi = api; + config = api.LoadModConfig("claimlinkvisualizer.json") ?? new ClaimLinkVisualizerConfig(); + api.StoreModConfig(config, "claimlinkvisualizer.json"); + + channel = api.Network.RegisterChannel(ClaimLinkVisualizerNetwork.ChannelName) + .RegisterMessageType() + .RegisterMessageType() + .SetMessageHandler(OnBufferResponse); + + api.Input.RegisterHotKey( + "claimlinkvisualizertoggle", + "Toggle ClaimLink buffer visualization", + GlKeys.R, + HotkeyType.CharacterControls, + ctrlPressed: true + ); + api.Input.SetHotKeyHandler("claimlinkvisualizertoggle", OnToggle); + } + + private static bool OnToggle(KeyCombination comb) + { + visible = !visible; + if (visible) + { + if (channel.Connected) + channel.SendPacket(new ClaimLinkBufferRequest()); + } + else + { + Clear(); + } + + return true; + } + + private static void OnBufferResponse(ClaimLinkBufferResponse response) + { + if (!visible) + return; + + List positions = new(); + List colors = new(); + + foreach (ClaimLinkBufferEntryDto entry in response.Entries) + { + int color = HashToColor(entry.GroupId); + foreach (ClaimLinkBufferCuboidDto cuboid in entry.Cuboids) + { + positions.Add(new BlockPos(cuboid.MinX, cuboid.MinY, cuboid.MinZ)); + positions.Add(new BlockPos(cuboid.MaxX, cuboid.MaxY, cuboid.MaxZ)); + colors.Add(color); + } + } + + capi.World.HighlightBlocks( + capi.World.Player, + HighlightSlotId, + positions, + colors, + EnumHighlightBlocksMode.Absolute, + EnumHighlightShape.Cubes + ); + } + + private static void Clear() => + capi.World.HighlightBlocks( + capi.World.Player, + HighlightSlotId, + new List(), + new List() + ); + + private static int HashToColor(int groupId) + { + unchecked + { + uint h = (uint)groupId * config.ColorHashSeed; + int r = (byte)(h >> 16); + int g = (byte)(h >> 8); + int b = (byte)h; + return ColorUtil.ColorFromRgba(r, g, b, 255); + } + } +} diff --git a/ClaimLink/ClaimLinkVisualizerConfig.cs b/ClaimLink/ClaimLinkVisualizerConfig.cs new file mode 100644 index 0000000..ed76fa8 --- /dev/null +++ b/ClaimLink/ClaimLinkVisualizerConfig.cs @@ -0,0 +1,6 @@ +namespace ClaimLink; + +internal class ClaimLinkVisualizerConfig +{ + public uint ColorHashSeed = 2654435761u; +} diff --git a/ClaimLink/CommandHookProxy.cs b/ClaimLink/CommandHookProxy.cs new file mode 100644 index 0000000..b4a557d --- /dev/null +++ b/ClaimLink/CommandHookProxy.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Reflection; +using Vintagestory.API.Common; + +namespace ClaimLink; + +internal delegate TextCommandResult? HookBeforeDelegate(TextCommandCallingArgs args); + +internal delegate void HookAfterDelegate(TextCommandCallingArgs args, TextCommandResult result); + +internal class CommandHookListenerProxy : DispatchProxy +{ + private string modId = null!; + private IReadOnlyList commands = null!; + private HookBeforeDelegate? before; + private HookAfterDelegate? after; + + internal static CommandHook.ICommandHookListener Create( + string modId, + IReadOnlyList commands, + HookBeforeDelegate? before, + HookAfterDelegate? after + ) + { + object proxy = Create()!; + CommandHookListenerProxy p = (CommandHookListenerProxy)proxy; + p.modId = modId; + p.commands = commands; + p.before = before; + p.after = after; + return (CommandHook.ICommandHookListener)proxy; + } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) => + targetMethod!.Name switch + { + "get_ModId" => modId, + "get_Commands" => commands, + "get_Registration" => new CommandHook.CommandRegistration( + before == null ? null : new CommandHook.CommandBeforeDelegate(before), + after == null ? null : new CommandHook.CommandAfterDelegate(after) + ), + _ => null, + }; +} diff --git a/ClaimLink/CommandListener.cs b/ClaimLink/CommandListener.cs index 3d904d2..07d1944 100644 --- a/ClaimLink/CommandListener.cs +++ b/ClaimLink/CommandListener.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using CommandHook; using Vintagestory.API.Common; using Vintagestory.API.Config; using Vintagestory.API.MathTools; @@ -7,14 +6,12 @@ using Vintagestory.API.Server; namespace ClaimLink; -public class CommandListener : ICommandHookListener +public class CommandListener { public string ModId => "claimlink"; public IReadOnlyList Commands => new[] { "land" }; - public CommandRegistration Registration => new(Before, After); - internal static Dictionary PendingLoads = new Dictionary(); internal static Dictionary?> PendingSaveChecks = new Dictionary?>(); @@ -57,13 +54,13 @@ public class CommandListener : ICommandHookListener } } - private TextCommandResult? Before(TextCommandCallingArgs args) + internal TextCommandResult? Before(TextCommandCallingArgs args) { Dispatch(args); return null; } - private void After(TextCommandCallingArgs args, TextCommandResult result) + internal void After(TextCommandCallingArgs args, TextCommandResult result) { if (args.Caller.Player == null) return; @@ -276,19 +273,17 @@ public class CommandListener : ICommandHookListener } } -public class GroupCommandListener : ICommandHookListener +public class GroupCommandListener { public string ModId => "claimlink.group"; public IReadOnlyList Commands => new[] { "group" }; - public CommandRegistration Registration => new(Before, After); - internal static Dictionary PendingDisbands = new Dictionary(); internal static Dictionary PendingMembershipChecks = new Dictionary(); - private TextCommandResult? Before(TextCommandCallingArgs args) + internal TextCommandResult? Before(TextCommandCallingArgs args) { if (args.Caller.Player == null) return null; @@ -370,7 +365,7 @@ public class GroupCommandListener : ICommandHookListener PendingMembershipChecks[callerUid] = (targetUid, (int)groupId); } - private void After(TextCommandCallingArgs args, TextCommandResult result) + internal void After(TextCommandCallingArgs args, TextCommandResult result) { if (args.Caller.Player == null) return; diff --git a/ClaimLink/modinfo.json b/ClaimLink/modinfo.json index c824731..db6d56d 100644 --- a/ClaimLink/modinfo.json +++ b/ClaimLink/modinfo.json @@ -3,14 +3,13 @@ "type": "Code", "modid": "claimlink", "name": "ClaimLink", - "side": "Server", + "side": "Universal", "authors": [ "anth64" ], "description": "Link claims together with groups.", "version": "0.5.1", "dependencies": { - "game": "1.22.5", - "commandhook": "2.1.0" + "game": "1.22.5" } }