feat: convert to Universal mod, merge ClaimLinkVisualizer in, keep CommandHook as a true soft dependency via runtime proxy

This commit is contained in:
2026-08-25 23:14:58 +02:00
parent 8f0e4bd687
commit b14eb5c538
6 changed files with 216 additions and 30 deletions
+56 -16
View File
@@ -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(
+101
View File
@@ -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<ClaimLinkVisualizerConfig>("claimlinkvisualizer.json") ?? new ClaimLinkVisualizerConfig();
api.StoreModConfig(config, "claimlinkvisualizer.json");
channel = api.Network.RegisterChannel(ClaimLinkVisualizerNetwork.ChannelName)
.RegisterMessageType<ClaimLinkBufferRequest>()
.RegisterMessageType<ClaimLinkBufferResponse>()
.SetMessageHandler<ClaimLinkBufferResponse>(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<BlockPos> positions = new();
List<int> 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<BlockPos>(),
new List<int>()
);
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);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace ClaimLink;
internal class ClaimLinkVisualizerConfig
{
public uint ColorHashSeed = 2654435761u;
}
+45
View File
@@ -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<string> commands = null!;
private HookBeforeDelegate? before;
private HookAfterDelegate? after;
internal static CommandHook.ICommandHookListener Create(
string modId,
IReadOnlyList<string> commands,
HookBeforeDelegate? before,
HookAfterDelegate? after
)
{
object proxy = Create<CommandHook.ICommandHookListener, CommandHookListenerProxy>()!;
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,
};
}
+6 -11
View File
@@ -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<string> Commands => new[] { "land" };
public CommandRegistration Registration => new(Before, After);
internal static Dictionary<string, int> PendingLoads = new Dictionary<string, int>();
internal static Dictionary<string, List<Cuboidi>?> PendingSaveChecks =
new Dictionary<string, List<Cuboidi>?>();
@@ -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<string> Commands => new[] { "group" };
public CommandRegistration Registration => new(Before, After);
internal static Dictionary<string, int> PendingDisbands = new Dictionary<string, int>();
internal static Dictionary<string, (string AffectedUid, int GroupId)> PendingMembershipChecks =
new Dictionary<string, (string, int)>();
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;
+2 -3
View File
@@ -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"
}
}