docs(CommandHookModSystem): add XML doc comments to Register/Unregister and internal comments to Rebuild, FireBefore, FireAfter, CommandListEquals
This commit is contained in:
@@ -7,6 +7,20 @@ using Vintagestory.API.Server;
|
||||
|
||||
namespace CommandHook;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side event bus for chat commands. Other mods register an
|
||||
/// <see cref="ICommandHookListener"/> here to get Before/After callbacks on
|
||||
/// specific command names, namespaced by <see cref="ICommandHookListener.ModId"/>
|
||||
/// so multiple mods can watch the same command without stepping on each other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Patches <c>ChatCommandApi.Execute</c> via Harmony. Server only, see
|
||||
/// <see cref="ShouldLoad"/>. A listener's <see cref="ICommandHookListener.Commands"/>
|
||||
/// and <see cref="ICommandHookListener.Registration"/> are read at registration
|
||||
/// time and cached in a <see cref="System.Collections.Frozen.FrozenDictionary{TKey,TValue}"/>
|
||||
/// for fast lookup on the command dispatch path. If you change which commands a
|
||||
/// listener watches, call <see cref="Register"/> again with the updated list.
|
||||
/// </remarks>
|
||||
public class CommandHookModSystem : ModSystem
|
||||
{
|
||||
internal static CommandHookModSystem? Instance;
|
||||
@@ -38,6 +52,17 @@ public class CommandHookModSystem : ModSystem
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a listener for the commands it returns from <see cref="ICommandHookListener.Commands"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Calling this again for a <see cref="ICommandHookListener.ModId"/> that is already
|
||||
/// registered replaces its previous registration. If the new <see cref="ICommandHookListener.Commands"/>
|
||||
/// list is identical to the existing one this is a no-op, no rebuild happens.
|
||||
/// If <paramref name="listener"/> has a null or empty <see cref="ICommandHookListener.Commands"/>
|
||||
/// list, this just calls <see cref="Unregister"/> instead.
|
||||
/// </remarks>
|
||||
/// <param name="listener">The listener to register or update.</param>
|
||||
public void Register(ICommandHookListener listener)
|
||||
{
|
||||
var commands = listener.Commands;
|
||||
@@ -66,6 +91,11 @@ public class CommandHookModSystem : ModSystem
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a listener's registration entirely. Safe to call on a listener
|
||||
/// that was never registered, this is a no-op in that case.
|
||||
/// </summary>
|
||||
/// <param name="listener">The listener to remove, matched by <see cref="ICommandHookListener.ModId"/>.</param>
|
||||
public void Unregister(ICommandHookListener listener)
|
||||
{
|
||||
int index = listeners.FindIndex(l => l.ModId == listener.ModId);
|
||||
@@ -77,6 +107,14 @@ public class CommandHookModSystem : ModSystem
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
// Rebuilds the dispatch table from listeners. Runs unconditionally and
|
||||
// immediately whenever Register/Unregister actually changes something,
|
||||
// no batching or startup-phase awareness. Command list sizes here are
|
||||
// small (a handful of mods, a handful of commands each), so the realloc
|
||||
// on every change is cheap and not worth the complexity of an incremental
|
||||
// update. The two-level structure (command -> ModId -> registration) is
|
||||
// what FireBefore/FireAfter walk on the hot path, so it's built once here
|
||||
// instead of being derived per-dispatch.
|
||||
private void Rebuild()
|
||||
{
|
||||
var builder = new Dictionary<string, Dictionary<string, CommandRegistration>>();
|
||||
@@ -108,6 +146,13 @@ public class CommandHookModSystem : ModSystem
|
||||
registrations = pruned.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
// Called from ChatCommandApiPatch.Prefix before the real command executes.
|
||||
// CommandData is passed by ref the whole way down, no allocation here.
|
||||
// Each listener's Before is isolated in its own try/catch so one mod
|
||||
// throwing doesn't stop the rest from running or break command dispatch
|
||||
// for the server. If a listener sets data.Cancel, we stop walking the
|
||||
// rest of the listeners immediately rather than letting them all run
|
||||
// against an already-cancelled command.
|
||||
internal bool FireBefore(string commandName, ref CommandData data)
|
||||
{
|
||||
if (registrations.TryGetValue(commandName, out var mods))
|
||||
@@ -131,6 +176,11 @@ public class CommandHookModSystem : ModSystem
|
||||
return data.Cancel;
|
||||
}
|
||||
|
||||
// Called from the wrapped onCommandComplete in ChatCommandApiPatch.Prefix,
|
||||
// only on the path where the command actually ran (Before didn't cancel).
|
||||
// Same per-listener try/catch as FireBefore, but there's no early-out here
|
||||
// since cancelling after the fact doesn't mean anything, the command
|
||||
// already ran.
|
||||
internal void FireAfter(string commandName, ref CommandData data, TextCommandResult result)
|
||||
{
|
||||
if (registrations.TryGetValue(commandName, out var mods))
|
||||
@@ -149,6 +199,10 @@ public class CommandHookModSystem : ModSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Order-sensitive on purpose. Register only treats an update as a no-op
|
||||
// if the list is identical in order and content, a reordered list still
|
||||
// triggers a rebuild. Cheap to check (just an index walk, no allocation)
|
||||
// and correctness here matters more than being lenient about ordering.
|
||||
private static bool CommandListEquals(IReadOnlyList<string> a, IReadOnlyList<string> b)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
|
||||
Reference in New Issue
Block a user