From b0b46f01cb58afa65a13288a5af731ce91e3bf40 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 13 Oct 2025 23:35:41 +0800 Subject: [PATCH 1/9] refactor: Complete initial migration --- .../ConsoleOutput/ConsoleOutputCallback.cs | 75 --------- .../ConsoleOutput/ConsoleOutputService.cs | 47 +----- .../Modules/Engine/CommandTracked.cs | 150 ++++-------------- .../EventParams/OnCommandExecuteHookEvent.cs | 12 ++ .../EventParams/OnConsoleOutputEvent.cs | 9 ++ .../Modules/Events/EventPublisher.cs | 28 +++- .../Modules/Events/EventSubscriber.cs | 26 +++ .../Services/CoreHookService.cs | 64 ++++++++ .../ConsoleOutput/IConsoleOutputService.cs | 36 ++--- .../Modules/Events/EventDelegates.cs | 10 ++ .../EventParams/IOnCommandExecuteHookEvent.cs | 19 +++ .../EventParams/IOnConsoleOutputEvent.cs | 12 ++ .../Modules/Events/IEventSubscriber.cs | 10 ++ managed/src/TestPlugin/TestPlugin.cs | 17 +- 14 files changed, 247 insertions(+), 268 deletions(-) delete mode 100644 managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs create mode 100644 managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs create mode 100644 managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnConsoleOutputEvent.cs create mode 100644 managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs create mode 100644 managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnConsoleOutputEvent.cs diff --git a/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs deleted file mode 100644 index 1b46d074d..000000000 --- a/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Runtime.InteropServices; -using SwiftlyS2.Core.Natives; -using SwiftlyS2.Shared.ConsoleOutput; -using Microsoft.Extensions.Logging; -using SwiftlyS2.Shared.Profiler; - -namespace SwiftlyS2.Core.ConsoleOutput; - -internal delegate void ConsoleOutputListenerCallbackDelegate(nint message); - -internal abstract class ConsoleOutputCallbackBase : IDisposable -{ - public Guid Guid { get; protected init; } - public IContextedProfilerService Profiler { get; } - public ILoggerFactory LoggerFactory { get; } - - protected ConsoleOutputCallbackBase(ILoggerFactory loggerFactory, IContextedProfilerService profiler) - { - LoggerFactory = loggerFactory; - Profiler = profiler; - } - - public abstract void Dispose(); -} - -internal class ConsoleOutputListenerCallback : ConsoleOutputCallbackBase -{ - private IConsoleOutputService.ConsoleOutputHandler _handler; - private ConsoleOutputListenerCallbackDelegate _unmanagedCallback; - private nint _unmanagedCallbackPtr; - private ulong _nativeListenerId; - private ILogger _logger; - - public ConsoleOutputListenerCallback(IConsoleOutputService.ConsoleOutputHandler handler, ILoggerFactory loggerFactory, IContextedProfilerService profiler) - : base(loggerFactory, profiler) - { - _logger = LoggerFactory.CreateLogger(); - Guid = Guid.NewGuid(); - - _handler = handler; - - _unmanagedCallback = (messagePtr) => - { - try - { - var category = "ConsoleOutputListenerCallback"; - Profiler.StartRecording(category); - var messageString = Marshal.PtrToStringUTF8(messagePtr)!; - _handler(messageString); - Profiler.StopRecording(category); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to handle console output listener."); - } - }; - - _unmanagedCallbackPtr = Marshal.GetFunctionPointerForDelegate(_unmanagedCallback); - _nativeListenerId = NativeConsoleOutput.AddConsoleListener(_unmanagedCallbackPtr); - } - - public override void Dispose() - { - try - { - NativeConsoleOutput.RemoveConsoleListener(_nativeListenerId); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to unregister console output listener."); - } - - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputService.cs b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputService.cs index 79f6ee266..4eeba9eb9 100644 --- a/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputService.cs @@ -7,45 +7,8 @@ namespace SwiftlyS2.Core.ConsoleOutput; internal class ConsoleOutputService : IConsoleOutputService, IDisposable { - private List _callbacks = new(); - private ILogger _logger { get; init; } - private ILoggerFactory _loggerFactory { get; init; } - private IContextedProfilerService _profiler { get; init; } - - private object _lock = new(); - - public ConsoleOutputService(ILogger logger, ILoggerFactory loggerFactory, IContextedProfilerService profiler) - { - _logger = logger; - _loggerFactory = loggerFactory; - _profiler = profiler; - } - - public Guid RegisterConsoleOutputListener(IConsoleOutputService.ConsoleOutputHandler handler) - { - var callback = new ConsoleOutputListenerCallback(handler, _loggerFactory, _profiler); - lock (_lock) - { - _callbacks.Add(callback); - } - - return callback.Guid; - } - - public void UnregisterConsoleOutputListener(Guid guid) + public ConsoleOutputService() { - lock (_lock) - { - _callbacks.RemoveAll(callback => - { - if (callback.Guid == guid) - { - callback.Dispose(); - return true; - } - return false; - }); - } } public bool IsFilterEnabled() @@ -80,14 +43,6 @@ public void WriteToServerConsole(string message) public void Dispose() { - lock (_lock) - { - foreach (var callback in _callbacks) - { - callback.Dispose(); - } - _callbacks.Clear(); - } GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs index 6788fcdd3..5d70aaefd 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs @@ -14,33 +14,6 @@ namespace SwiftlyS2.Core.Modules.Engine; internal sealed class CommandTracked : IDisposable { - /* - Original function in engine2.dll: __int64 sub_1C0CD0(__int64 a1, int a2, unsigned int a3, ...) - This is a variadic function, but we only need the first two variable arguments (v55, v57) - - __int64 sub_1C0CD0(__int64 a1, int a2, unsigned int a3, ...) - { - ... - - va_list va; // [rsp+D28h] [rbp+D28h] - __int64 v55; // [rsp+E28h] [rbp+D28h] BYREF - va_list va1; // [rsp+E28h] [rbp+D28h] - - ... - - va_start(va1, a3); - va_start(va, a3); - v55 = va_arg(va1, _QWORD); - v57 = va_arg(va1, _QWORD); - - ... - } - - So we model it as a fixed 5-parameter function for interop purposes - */ - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate nint ExecuteCommandDelegate(nint a1, int a2, uint a3, nint a4, nint a5); - private sealed record CommandIdContainer(Guid Value) { public static readonly CommandIdContainer Empty = new(Guid.Empty); @@ -61,13 +34,6 @@ private readonly record struct ExecutingCommand(Action Callback) private readonly IConsoleOutputService consoleOutputService; private readonly IGameDataService gameDataService; private volatile CommandIdContainer currentCommandContainer; - private volatile bool hooksInitialized; - private readonly object initializationLock = new(); - private static readonly object commandNameOffsetLock = new(); - private Guid hookId; - private Guid outputListenerId; - private IUnmanagedFunction? executeCommandHook; - private static int commandNameOffset = 0; public CommandTracked(ISwiftlyCore swiftlyCore) { @@ -78,87 +44,38 @@ public CommandTracked(ISwiftlyCore swiftlyCore) activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); currentCommandContainer = CommandIdContainer.Empty; - lock (commandNameOffsetLock) - { - if (commandNameOffset == 0) - { - commandNameOffset = NativeOffsets.Fetch("CommandNameOffset"); - } - } StartCleanupTimer(); } - private void EnsureHooksInitialized() - { - if (hooksInitialized || gameDataService == null) return; - - lock (initializationLock) - { - if (hooksInitialized) return; - - - var signature = gameDataService.GetSignature("Cmd_ExecuteCommand"); - if (signature == nint.Zero) return; - - executeCommandHook = memoryService.GetUnmanagedFunctionByAddress(signature); - hookId = executeCommandHook.AddHook((next) => - { - return (a1, a2, a3, a4, a5) => - { - ProcessCommandStart(a5); - var result = next()(a1, a2, a3, a4, a5); - ProcessCommandEnd(); - return result; - }; - }); - - outputListenerId = consoleOutputService.RegisterConsoleOutputListener((message) => - { - if (disposed) return; - - var commandId = currentCommandContainer?.Value ?? Guid.Empty; - if (commandId == Guid.Empty) return; - - if (activeCommands.TryGetValue(commandId, out var command) && command.Output.Count < 100) - { - command.Output.Enqueue(message); - } - }); - - Thread.MemoryBarrier(); - hooksInitialized = true; - } - } - private void ProcessCommandStart(nint a5) { - var commandNamePtr = GetCommandNamePointer(a5); - if (commandNamePtr == nint.Zero) return; - - var commandStr = Marshal.PtrToStringAnsi(commandNamePtr); - if (string.IsNullOrEmpty(commandStr) || !commandStr.Contains("^wb^")) - { - Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); - return; - } - - if (pendingCallbacks.TryDequeue(out var callback)) - { - var newCommandId = Guid.NewGuid(); - var newCommand = new ExecutingCommand(callback); - - if (activeCommands.TryAdd(newCommandId, newCommand)) - { - var newContainer = new CommandIdContainer(newCommandId); - Interlocked.Exchange(ref currentCommandContainer, newContainer); - CleanCommandName(commandNamePtr, commandStr); - } - } - else - { - Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); - } + // var commandNamePtr = GetCommandNamePointer(a5); + // if (commandNamePtr == nint.Zero) return; + + // var commandStr = Marshal.PtrToStringAnsi(commandNamePtr); + // if (string.IsNullOrEmpty(commandStr) || !commandStr.Contains("^wb^")) + // { + // Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); + // return; + // } + + // if (pendingCallbacks.TryDequeue(out var callback)) + // { + // var newCommandId = Guid.NewGuid(); + // var newCommand = new ExecutingCommand(callback); + + // if (activeCommands.TryAdd(newCommandId, newCommand)) + // { + // var newContainer = new CommandIdContainer(newCommandId); + // Interlocked.Exchange(ref currentCommandContainer, newContainer); + // CleanCommandName(commandNamePtr, commandStr); + // } + // } + // else + // { + // Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); + // } } private void ProcessCommandEnd() @@ -182,15 +99,6 @@ private void ProcessCommandEnd() } } - private static nint GetCommandNamePointer(nint a5) - { - if (a5 == nint.Zero || a5 >= nint.MaxValue || commandNameOffset == 0) return nint.Zero; - var basePtr = Marshal.ReadIntPtr(new nint(a5 + commandNameOffset)); - - if (basePtr == nint.Zero || basePtr >= nint.MaxValue) return nint.Zero; - return Marshal.ReadIntPtr(basePtr); - } - private static void CleanCommandName(nint commandPtr, string commandStr) { var cleanedCommand = commandStr.Replace("^wb^", string.Empty); @@ -236,8 +144,6 @@ public void EnqueueCommand(Action callback) { if (disposed || callback == null) return; - EnsureHooksInitialized(); - pendingCallbacks.Enqueue(callback); } @@ -247,8 +153,8 @@ public void Dispose() disposed = true; cancellationTokenSource.Cancel(); - executeCommandHook?.RemoveHook(hookId); - consoleOutputService.UnregisterConsoleOutputListener(outputListenerId); + // executeCommandHook?.RemoveHook(hookId); + // consoleOutputService.UnregisterConsoleOutputListener(outputListenerId); while (pendingCallbacks.TryDequeue(out _)) { } activeCommands.Clear(); diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs new file mode 100644 index 000000000..8321c59d8 --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs @@ -0,0 +1,12 @@ +using SwiftlyS2.Shared.Misc; +using SwiftlyS2.Shared.Events; + +namespace SwiftlyS2.Core.Events; + +internal class OnCommandExecuteHookEvent : IOnCommandExecuteHookEvent +{ + + public required string CommandName { get; set; } + + public required HookMode HookMode { get; set; } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnConsoleOutputEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnConsoleOutputEvent.cs new file mode 100644 index 000000000..a9e8ece04 --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnConsoleOutputEvent.cs @@ -0,0 +1,9 @@ +using SwiftlyS2.Shared.Events; + +namespace SwiftlyS2.Core.Events; + +internal class OnConsoleOutputEvent : IOnConsoleOutputEvent +{ + + public required string Message { get; set; } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs index f6e538d84..210f80223 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs @@ -43,10 +43,10 @@ public static void Register() { NativeEvents.RegisterOnClientProcessUsercmdsCallback((nint)(delegate* unmanaged)&OnClientProcessUsercmds); NativeEvents.RegisterOnEntityTakeDamageCallback((nint)(delegate* unmanaged)&OnEntityTakeDamage); NativeEvents.RegisterOnPrecacheResourceCallback((nint)(delegate* unmanaged)&OnPrecacheResource); + NativeConsoleOutput.AddConsoleListener((nint)(delegate* unmanaged)&OnConsoleOutput); } } - [UnmanagedCallersOnly] public static void OnTick(byte simulating, byte first, byte last) { @@ -376,4 +376,30 @@ public static void InvokeOnCanAcquireHook(OnItemServicesCanAcquireHookEvent @eve return; } } + + [UnmanagedCallersOnly] + public static void OnConsoleOutput(nint messagePtr) { + if (_subscribers.Count == 0) return; + try { + OnConsoleOutputEvent @event = new() { + Message = Marshal.PtrToStringUTF8(messagePtr) ?? string.Empty + }; + foreach (var subscriber in _subscribers) { + subscriber.InvokeOnConsoleOutput(@event); + } + } catch (Exception e) { + AnsiConsole.WriteException(e); + } + } + + public static void InvokeOnCommandExecuteHook(OnCommandExecuteHookEvent @event) { + if (_subscribers.Count == 0) return; + try { + foreach (var subscriber in _subscribers) { + subscriber.InvokeOnCommandExecuteHook(@event); + } + } catch (Exception e) { + AnsiConsole.WriteException(e); + } + } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs index 56fcb1c2a..bfa11f77d 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs @@ -42,6 +42,8 @@ public EventSubscriber(CoreContext id, IContextedProfilerService profiler, ILogg public event EventDelegates.OnEntityTakeDamage? OnEntityTakeDamage; public event EventDelegates.OnPrecacheResource? OnPrecacheResource; public event EventDelegates.OnItemServicesCanAcquireHook? OnItemServicesCanAcquireHook; + public event EventDelegates.OnConsoleOutput? OnConsoleOutput; + public event EventDelegates.OnCommandExecuteHook? OnCommandExecuteHook; public void Dispose() { EventPublisher.Unsubscribe(this); @@ -251,4 +253,28 @@ public void InvokeOnItemServicesCanAcquireHook(OnItemServicesCanAcquireHookEvent _Profiler.StopRecording("Event::OnItemServicesCanAcquireHook"); } } + + public void InvokeOnConsoleOutput(OnConsoleOutputEvent @event) { + try { + if (OnConsoleOutput == null) return; + _Profiler.StartRecording("Event::OnConsoleOutput"); + OnConsoleOutput?.Invoke(@event); + } catch (Exception e) { + _Logger.LogError(e, "Error invoking OnConsoleOutput."); + } finally { + _Profiler.StopRecording("Event::OnConsoleOutput"); + } + } + + public void InvokeOnCommandExecuteHook(OnCommandExecuteHookEvent @event) { + try { + if (OnCommandExecuteHook == null) return; + _Profiler.StartRecording("Event::OnCommandExecuteHook"); + OnCommandExecuteHook?.Invoke(@event); + } catch (Exception e) { + _Logger.LogError(e, "Error invoking OnCommandExecuteHook."); + } finally { + _Profiler.StopRecording("Event::OnCommandExecuteHook"); + } + } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs index 261330e62..9c09baeeb 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using SwiftlyS2.Core.Events; using SwiftlyS2.Core.Natives; @@ -17,9 +18,37 @@ public CoreHookService(ILogger logger, ISwiftlyCore core) { _Core = core; HookCanAcquire(); + HookCommandExecute(); } private delegate int CanAcquireDelegate(nint pItemServices, nint pEconItemView, nint acquireMethod, nint unk1); + /* + Original function in engine2.dll: __int64 sub_1C0CD0(__int64 a1, int a2, unsigned int a3, ...) + This is a variadic function, but we only need the first two variable arguments (v55, v57) + + __int64 sub_1C0CD0(__int64 a1, int a2, unsigned int a3, ...) + { + ... + + va_list va; // [rsp+D28h] [rbp+D28h] + __int64 v55; // [rsp+E28h] [rbp+D28h] BYREF + va_list va1; // [rsp+E28h] [rbp+D28h] + + ... + + va_start(va1, a3); + va_start(va, a3); + v55 = va_arg(va1, _QWORD); + v57 = va_arg(va1, _QWORD); + + ... + } + + So we model it as a fixed 5-parameter function for interop purposes + */ + private delegate nint ExecuteCommandDelegate(nint a1, int a2, uint a3, nint a4, nint a5); + private IUnmanagedFunction? _ExecuteCommand; + private Guid _ExecuteCommandGuid; private IUnmanagedFunction? _CanAcquire; private Guid _CanAcquireGuid; @@ -58,7 +87,42 @@ private void HookCanAcquire() { }); } + private void HookCommandExecute() { + + var address = _Core.GameData.GetSignature("Cmd_ExecuteCommand"); + + _Logger.LogInformation("Hooking Cmd_ExecuteCommand at {Address}", address); + var commandNameOffset = NativeOffsets.Fetch("CommandNameOffset"); + + _ExecuteCommand = _Core.Memory.GetUnmanagedFunctionByAddress(address); + _ExecuteCommandGuid = _ExecuteCommand.AddHook((next) => + { + return (a1, a2, a3, a4, a5) => + { + var commandName = (a5 != nint.Zero && a5 < nint.MaxValue && commandNameOffset != 0) switch { + true when Marshal.ReadIntPtr(new nint(a5 + commandNameOffset)) is var basePtr && basePtr != nint.Zero && basePtr < nint.MaxValue + => Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(basePtr)), + _ => null + } ?? string.Empty; + + var @event = new OnCommandExecuteHookEvent { + CommandName = commandName, + HookMode = HookMode.Pre + }; + EventPublisher.InvokeOnCommandExecuteHook(@event); + + var result = next()(a1, a2, a3, a4, a5); + + @event.HookMode = HookMode.Post; + EventPublisher.InvokeOnCommandExecuteHook(@event); + + return result; + }; + }); + } + public void Dispose() { _CanAcquire!.RemoveHook(_CanAcquireGuid); + _ExecuteCommand!.RemoveHook(_ExecuteCommandGuid); } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs b/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs index bb3646e83..5cbdd5da1 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs @@ -2,24 +2,24 @@ namespace SwiftlyS2.Shared.ConsoleOutput; public interface IConsoleOutputService { - /// - /// The handler for console output events. - /// - /// The console message. - delegate void ConsoleOutputHandler(string message); - - /// - /// Registers a console output listener. - /// - /// The handler to call when console output occurs. - /// A GUID that can be used to unregister the listener. - Guid RegisterConsoleOutputListener(ConsoleOutputHandler handler); - - /// - /// Unregisters a console output listener. - /// - /// The GUID returned from RegisterConsoleOutputListener. - void UnregisterConsoleOutputListener(Guid guid); + // /// + // /// The handler for console output events. + // /// + // /// The console message. + // delegate void ConsoleOutputHandler(string message); + + // /// + // /// Registers a console output listener. + // /// + // /// The handler to call when console output occurs. + // /// A GUID that can be used to unregister the listener. + // Guid RegisterConsoleOutputListener(ConsoleOutputHandler handler); + + // /// + // /// Unregisters a console output listener. + // /// + // /// The GUID returned from RegisterConsoleOutputListener. + // void UnregisterConsoleOutputListener(Guid guid); /// /// Gets whether console filtering is enabled. diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs index 3bd15779f..6321e6452 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs @@ -91,4 +91,14 @@ public class EventDelegates { /// Called when an item services can acquire hook is triggered. /// public delegate void OnItemServicesCanAcquireHook(IOnItemServicesCanAcquireHookEvent @event); + + /// + /// Called when a console output is received. + /// + public delegate void OnConsoleOutput(IOnConsoleOutputEvent @event); + + /// + /// Called when a command is executed. + /// + public delegate void OnCommandExecuteHook(IOnCommandExecuteHookEvent @event); } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs new file mode 100644 index 000000000..0006c2ff2 --- /dev/null +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs @@ -0,0 +1,19 @@ +using SwiftlyS2.Shared.Misc; + +namespace SwiftlyS2.Shared.Events; + +/// +/// Called when a command is executed. +/// +public interface IOnCommandExecuteHookEvent { + + /// + /// The command name. + /// + public string CommandName { get; } + + /// + /// The command arguments. + /// + public HookMode HookMode { get; } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnConsoleOutputEvent.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnConsoleOutputEvent.cs new file mode 100644 index 000000000..84ac21031 --- /dev/null +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnConsoleOutputEvent.cs @@ -0,0 +1,12 @@ +namespace SwiftlyS2.Shared.Events; + +/// +/// Called when a console output is received. +/// +public interface IOnConsoleOutputEvent { + + /// + /// The message of the console output. + /// + public string Message { get; } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs index eda77af4c..ffcafc58d 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs @@ -91,4 +91,14 @@ public interface IEventSubscriber { /// Called when an item services can acquire hook is triggered. /// public event EventDelegates.OnItemServicesCanAcquireHook? OnItemServicesCanAcquireHook; + + /// + /// Called when the game outputs a console message. + /// + public event EventDelegates.OnConsoleOutput? OnConsoleOutput; + + /// + /// Called when a command is executed. + /// + public event EventDelegates.OnCommandExecuteHook? OnCommandExecuteHook; } \ No newline at end of file diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index 1c6960a9f..0c09d5302 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -70,14 +70,19 @@ public void SoundCommand(ICommandContext context) public override void Load(bool hotReload) { - // var consoleListenerId = Core.ConsoleOutput.RegisterConsoleOutputListener((message) => + Core.Event.OnConsoleOutput += (@event) => + { + Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); + }; + + Core.Event.OnCommandExecuteHook += (@event) => + { + Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.CommandName}"); + }; + // Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => // { - // Core.Logger.LogInformation($"message: {message}"); + // Console.WriteLine($"pong: {buffer}"); // }); - Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => - { - Console.WriteLine($"pong: {buffer}"); - }); Core.GameEvent.HookPre(@event => { From 71e7ac61ff47ee58e5ecbf4de6fb126673adb4fb Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 13 Oct 2025 23:53:31 +0800 Subject: [PATCH 2/9] feat: Allow setting CommandName --- .../EventParams/OnCommandExecuteHookEvent.cs | 12 +++++-- .../Services/CoreHookService.cs | 31 +++++++++++++------ .../EventParams/IOnCommandExecuteHookEvent.cs | 11 +++++-- managed/src/TestPlugin/TestPlugin.cs | 11 ++++--- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs index 8321c59d8..981a1cfd6 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs @@ -5,8 +5,16 @@ namespace SwiftlyS2.Core.Events; internal class OnCommandExecuteHookEvent : IOnCommandExecuteHookEvent { + public required string OriginalName { get; init; } + public string CommandName { get; set; } = string.Empty; - public required string CommandName { get; set; } + public required HookMode HookMode { get; init; } - public required HookMode HookMode { get; set; } + public bool Intercepted { get; set; } = false; + + public void SetCommandName(string name) { + if (HookMode == HookMode.Post) return; + CommandName = name; + Intercepted = true; + } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs index 9c09baeeb..0873454c7 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using SwiftlyS2.Core.Events; @@ -99,22 +100,34 @@ private void HookCommandExecute() { { return (a1, a2, a3, a4, a5) => { - var commandName = (a5 != nint.Zero && a5 < nint.MaxValue && commandNameOffset != 0) switch { + var (commandName, commandPtr) = (a5 != nint.Zero && a5 < nint.MaxValue && commandNameOffset != 0) switch { true when Marshal.ReadIntPtr(new nint(a5 + commandNameOffset)) is var basePtr && basePtr != nint.Zero && basePtr < nint.MaxValue - => Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(basePtr)), - _ => null - } ?? string.Empty; + => (Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(basePtr)) ?? string.Empty, Marshal.ReadIntPtr(basePtr)), + _ => (string.Empty, nint.Zero) + }; - var @event = new OnCommandExecuteHookEvent { - CommandName = commandName, + var preEvent = new OnCommandExecuteHookEvent { + OriginalName = commandName, HookMode = HookMode.Pre }; - EventPublisher.InvokeOnCommandExecuteHook(@event); + EventPublisher.InvokeOnCommandExecuteHook(preEvent); + + if (preEvent.Intercepted && preEvent.CommandName.Length < commandName.Length) { + var newCommandName = Encoding.ASCII.GetBytes(preEvent.CommandName + "\0"); + var maxLength = Encoding.ASCII.GetByteCount(commandName + "\0"); + + if (newCommandName.Length <= maxLength) { + Marshal.Copy(newCommandName, 0, commandPtr, newCommandName.Length); + } + } var result = next()(a1, a2, a3, a4, a5); - @event.HookMode = HookMode.Post; - EventPublisher.InvokeOnCommandExecuteHook(@event); + var postEvent = new OnCommandExecuteHookEvent { + OriginalName = commandName, + HookMode = HookMode.Post + }; + EventPublisher.InvokeOnCommandExecuteHook(postEvent); return result; }; diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs index 0006c2ff2..3ad311a2a 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs @@ -8,12 +8,19 @@ namespace SwiftlyS2.Shared.Events; public interface IOnCommandExecuteHookEvent { /// - /// The command name. + /// The original command name. /// - public string CommandName { get; } + public string OriginalName { get; } /// /// The command arguments. /// public HookMode HookMode { get; } + + /// + /// Intercept and modify the command name. + /// This will modify the command name and stop the following hooks and original function. + /// + /// The name to modify. + public void SetCommandName(string name); } \ No newline at end of file diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index 0c09d5302..dbddf9d77 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -70,14 +70,15 @@ public void SoundCommand(ICommandContext context) public override void Load(bool hotReload) { - Core.Event.OnConsoleOutput += (@event) => - { - Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); - }; + // Core.Event.OnConsoleOutput += (@event) => + // { + // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); + // }; Core.Event.OnCommandExecuteHook += (@event) => { - Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.CommandName}"); + Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName}"); + @event.SetCommandName("test"); }; // Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => // { From 5f6c4c7bb9f4246023d6d7824e1e8e6101d43a95 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 00:14:53 +0800 Subject: [PATCH 3/9] refactor: Complete CommandTracked migration --- managed/src/SwiftlyS2.Core/Bootstrap.cs | 2 +- .../Hosting/CommandTrackedServiceInjection.cs | 17 +++ ...andTracked.cs => CommandTrackedService.cs} | 121 +++++++++--------- .../Modules/Engine/EngineService.cs | 2 +- .../SwiftlyS2.Core/Services/StartupService.cs | 7 +- managed/src/TestPlugin/TestPlugin.cs | 16 +-- 6 files changed, 93 insertions(+), 72 deletions(-) create mode 100644 managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs rename managed/src/SwiftlyS2.Core/Modules/Engine/{CommandTracked.cs => CommandTrackedService.cs} (58%) diff --git a/managed/src/SwiftlyS2.Core/Bootstrap.cs b/managed/src/SwiftlyS2.Core/Bootstrap.cs index b2b3b6d38..55ae3bdcb 100644 --- a/managed/src/SwiftlyS2.Core/Bootstrap.cs +++ b/managed/src/SwiftlyS2.Core/Bootstrap.cs @@ -66,7 +66,7 @@ public static void Start(IntPtr nativeTable, int nativeTableSize, string basePat .AddPermissionManager() .AddCoreHookService() .AddMenuService() - .AddSingleton() + .AddCommandTrackedService() .AddSwiftlyCore(basePath); }) .Build(); diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs new file mode 100644 index 000000000..f7d7349e2 --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using SwiftlyS2.Core.Modules.Engine; + +namespace SwiftlyS2.Core.Hosting; + +internal static class CommandTrackedServiceInjection +{ + public static IServiceCollection AddCommandTrackedService(this IServiceCollection self) + { + return self.AddSingleton(); + } + + public static void UseCommandTrackedService(this IServiceProvider self) + { + self.GetRequiredService(); + } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs similarity index 58% rename from managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs rename to managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs index 5d70aaefd..252a7a4e6 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTracked.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs @@ -6,13 +6,12 @@ using System.Runtime.InteropServices; using SwiftlyS2.Shared; using SwiftlyS2.Core.Natives; -using SwiftlyS2.Shared.Memory; -using SwiftlyS2.Shared.Services; -using SwiftlyS2.Shared.ConsoleOutput; +using SwiftlyS2.Shared.Events; +using SwiftlyS2.Shared.Misc; namespace SwiftlyS2.Core.Modules.Engine; -internal sealed class CommandTracked : IDisposable +internal sealed class CommandTrackedService : IDisposable { private sealed record CommandIdContainer(Guid Value) { @@ -26,59 +25,79 @@ private readonly record struct ExecutingCommand(Action Callback) public bool IsExpired => DateTime.UtcNow - Created > TimeSpan.FromMilliseconds(5000); } - private volatile bool disposed; + private volatile CommandIdContainer currentCommandContainer; + private readonly ConcurrentDictionary activeCommands; private readonly CancellationTokenSource cancellationTokenSource; private readonly ConcurrentQueue> pendingCallbacks; - private readonly ConcurrentDictionary activeCommands; - private readonly IMemoryService memoryService; - private readonly IConsoleOutputService consoleOutputService; - private readonly IGameDataService gameDataService; - private volatile CommandIdContainer currentCommandContainer; + private readonly IEventSubscriber eventSubscriber; + private volatile bool disposed; - public CommandTracked(ISwiftlyCore swiftlyCore) + public CommandTrackedService(ISwiftlyCore core) { - memoryService = swiftlyCore.Memory; - consoleOutputService = swiftlyCore.ConsoleOutput; - gameDataService = swiftlyCore.GameData; + this.eventSubscriber = core.Event; pendingCallbacks = new ConcurrentQueue>(); activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); currentCommandContainer = CommandIdContainer.Empty; + eventSubscriber.OnCommandExecuteHook += ProcessCommand; + eventSubscriber.OnConsoleOutput += ProcessOutput; + StartCleanupTimer(); } - private void ProcessCommandStart(nint a5) + private void ProcessCommand(IOnCommandExecuteHookEvent @event) { - // var commandNamePtr = GetCommandNamePointer(a5); - // if (commandNamePtr == nint.Zero) return; - - // var commandStr = Marshal.PtrToStringAnsi(commandNamePtr); - // if (string.IsNullOrEmpty(commandStr) || !commandStr.Contains("^wb^")) - // { - // Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); - // return; - // } - - // if (pendingCallbacks.TryDequeue(out var callback)) - // { - // var newCommandId = Guid.NewGuid(); - // var newCommand = new ExecutingCommand(callback); - - // if (activeCommands.TryAdd(newCommandId, newCommand)) - // { - // var newContainer = new CommandIdContainer(newCommandId); - // Interlocked.Exchange(ref currentCommandContainer, newContainer); - // CleanCommandName(commandNamePtr, commandStr); - // } - // } - // else - // { - // Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); - // } + if (string.IsNullOrEmpty(@event.OriginalName) || !@event.OriginalName.Contains("^wb^")) + { + Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); + return; + } + + if (@event.HookMode == HookMode.Pre) + { + ProcessCommandStart(@event); + } + else if (@event.HookMode == HookMode.Post) + { + ProcessCommandEnd(@event); + } } - private void ProcessCommandEnd() + private void ProcessOutput(IOnConsoleOutputEvent @event) + { + if (disposed) return; + + var commandId = currentCommandContainer?.Value ?? Guid.Empty; + if (commandId == Guid.Empty) return; + + if (activeCommands.TryGetValue(commandId, out var command) && command.Output.Count < 100) + { + command.Output.Enqueue(@event.Message); + } + } + + private void ProcessCommandStart(IOnCommandExecuteHookEvent @event) + { + if (pendingCallbacks.TryDequeue(out var callback)) + { + var newCommandId = Guid.NewGuid(); + var newCommand = new ExecutingCommand(callback); + + if (activeCommands.TryAdd(newCommandId, newCommand)) + { + var newContainer = new CommandIdContainer(newCommandId); + Interlocked.Exchange(ref currentCommandContainer, newContainer); + @event.SetCommandName(@event.OriginalName.Replace("^wb^", string.Empty)); + } + } + else + { + Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); + } + } + + private void ProcessCommandEnd(IOnCommandExecuteHookEvent @event) { var previousContainer = Interlocked.Exchange(ref currentCommandContainer, CommandIdContainer.Empty); var commandId = previousContainer?.Value ?? Guid.Empty; @@ -99,20 +118,6 @@ private void ProcessCommandEnd() } } - private static void CleanCommandName(nint commandPtr, string commandStr) - { - var cleanedCommand = commandStr.Replace("^wb^", string.Empty); - if (cleanedCommand.Length >= commandStr.Length) return; - - var cleanedBytes = Encoding.ASCII.GetBytes(cleanedCommand + "\0"); - var maxLength = Encoding.ASCII.GetByteCount(commandStr + "\0"); - - if (cleanedBytes.Length <= maxLength) - { - Marshal.Copy(cleanedBytes, 0, commandPtr, cleanedBytes.Length); - } - } - private void StartCleanupTimer() { Task.Run(async () => @@ -153,8 +158,8 @@ public void Dispose() disposed = true; cancellationTokenSource.Cancel(); - // executeCommandHook?.RemoveHook(hookId); - // consoleOutputService.UnregisterConsoleOutputListener(outputListenerId); + eventSubscriber.OnCommandExecuteHook -= ProcessCommand; + eventSubscriber.OnConsoleOutput -= ProcessOutput; while (pendingCallbacks.TryDequeue(out _)) { } activeCommands.Clear(); diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs index 1e39da134..e084d42d0 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs @@ -33,7 +33,7 @@ public void ExecuteCommandWithBuffer(string command, Action bufferCallba { try { - var commandTracked = serviceProvider.GetService(); + var commandTracked = serviceProvider.GetService(); commandTracked?.EnqueueCommand(bufferCallback); NativeEngineHelpers.ExecuteCommand($"{command}^wb^"); } diff --git a/managed/src/SwiftlyS2.Core/Services/StartupService.cs b/managed/src/SwiftlyS2.Core/Services/StartupService.cs index cc300e45e..77762f131 100644 --- a/managed/src/SwiftlyS2.Core/Services/StartupService.cs +++ b/managed/src/SwiftlyS2.Core/Services/StartupService.cs @@ -8,18 +8,17 @@ namespace SwiftlyS2.Core.Services; internal class StartupService : IHostedService { - private IServiceProvider _provider; - private CommandTracked _commandTracked; + private readonly IServiceProvider _provider; - public StartupService(IServiceProvider provider, CommandTracked commandTracked) + public StartupService(IServiceProvider provider) { _provider = provider; - _commandTracked = commandTracked; provider.UseCoreCommandService(); provider.UseCoreHookService(); provider.UsePermissionManager(); provider.UsePluginManager(); provider.UseMenuService(); + provider.UseCommandTrackedService(); // provider.UseTestService(); } diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index dbddf9d77..b6c7cbb80 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -75,15 +75,15 @@ public override void Load(bool hotReload) // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); // }; - Core.Event.OnCommandExecuteHook += (@event) => - { - Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName}"); - @event.SetCommandName("test"); - }; - // Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => + // Core.Event.OnCommandExecuteHook += (@event) => // { - // Console.WriteLine($"pong: {buffer}"); - // }); + // Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName}"); + // @event.SetCommandName("test"); + // }; + Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => + { + Console.WriteLine($"pong: {buffer}"); + }); Core.GameEvent.HookPre(@event => { From 4f99a5291b86e2302fd996d8e12b7ac249c3dc95 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 00:27:19 +0800 Subject: [PATCH 4/9] refactor: Minimize dependency injection --- .../Hosting/CommandTrackedServiceInjection.cs | 8 +++++++- .../Modules/Engine/CommandTrackedService.cs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs index f7d7349e2..4377c5456 100644 --- a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs +++ b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using SwiftlyS2.Core.Modules.Engine; +using SwiftlyS2.Shared; +using SwiftlyS2.Shared.Events; namespace SwiftlyS2.Core.Hosting; @@ -7,7 +9,11 @@ internal static class CommandTrackedServiceInjection { public static IServiceCollection AddCommandTrackedService(this IServiceCollection self) { - return self.AddSingleton(); + return self.AddSingleton(provider => + { + var swiftlyCore = provider.GetRequiredService(); + return new CommandTrackedService(swiftlyCore.Event); + }); } public static void UseCommandTrackedService(this IServiceProvider self) diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs index 252a7a4e6..9dfb2baa8 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs @@ -32,9 +32,9 @@ private readonly record struct ExecutingCommand(Action Callback) private readonly IEventSubscriber eventSubscriber; private volatile bool disposed; - public CommandTrackedService(ISwiftlyCore core) + public CommandTrackedService(IEventSubscriber eventSubscriber) { - this.eventSubscriber = core.Event; + this.eventSubscriber = eventSubscriber; pendingCallbacks = new ConcurrentQueue>(); activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); From fbd375b6302f6101f2d9ff6b31368140f9bbdd7d Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 09:44:22 +0800 Subject: [PATCH 5/9] fix: Inject ISwiftlyCore into CommandTrackedService --- .../Hosting/CommandTrackedServiceInjection.cs | 6 +----- .../SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs index 4377c5456..3d327aa65 100644 --- a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs +++ b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs @@ -9,11 +9,7 @@ internal static class CommandTrackedServiceInjection { public static IServiceCollection AddCommandTrackedService(this IServiceCollection self) { - return self.AddSingleton(provider => - { - var swiftlyCore = provider.GetRequiredService(); - return new CommandTrackedService(swiftlyCore.Event); - }); + return self.AddSingleton(); } public static void UseCommandTrackedService(this IServiceProvider self) diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs index 9dfb2baa8..1caca149c 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs @@ -32,9 +32,9 @@ private readonly record struct ExecutingCommand(Action Callback) private readonly IEventSubscriber eventSubscriber; private volatile bool disposed; - public CommandTrackedService(IEventSubscriber eventSubscriber) + public CommandTrackedService(ISwiftlyCore swiftlyCore) { - this.eventSubscriber = eventSubscriber; + this.eventSubscriber = swiftlyCore.Event; pendingCallbacks = new ConcurrentQueue>(); activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); From 71f5c60f5dc0447f3d32d6fa57459893af75158d Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 10:04:43 +0800 Subject: [PATCH 6/9] refactor: Resolve circular dependencies using Lazy loading --- .../Hosting/CommandTrackedServiceInjection.cs | 13 +++---- .../Modules/Engine/CommandTrackedService.cs | 38 ++++++++++++------- .../Modules/Engine/EngineService.cs | 16 +++----- .../SwiftlyS2.Core/Services/StartupService.cs | 2 +- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs index 3d327aa65..4c5bbe83e 100644 --- a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs +++ b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using SwiftlyS2.Core.Modules.Engine; using SwiftlyS2.Shared; -using SwiftlyS2.Shared.Events; +using SwiftlyS2.Core.Modules.Engine; namespace SwiftlyS2.Core.Hosting; @@ -9,11 +8,11 @@ internal static class CommandTrackedServiceInjection { public static IServiceCollection AddCommandTrackedService(this IServiceCollection self) { - return self.AddSingleton(); + return self.AddSingleton(provider => new CommandTrackedService(new Lazy(() => provider.GetRequiredService()))); } - public static void UseCommandTrackedService(this IServiceProvider self) - { - self.GetRequiredService(); - } + // public static void UseCommandTrackedService(this IServiceProvider self) + // { + // self.GetRequiredService(); + // } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs index 1caca149c..7969e3326 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs @@ -1,13 +1,8 @@ -using System; using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Collections.Concurrent; -using System.Runtime.InteropServices; using SwiftlyS2.Shared; -using SwiftlyS2.Core.Natives; -using SwiftlyS2.Shared.Events; using SwiftlyS2.Shared.Misc; +using SwiftlyS2.Shared.Events; namespace SwiftlyS2.Core.Modules.Engine; @@ -29,19 +24,18 @@ private readonly record struct ExecutingCommand(Action Callback) private readonly ConcurrentDictionary activeCommands; private readonly CancellationTokenSource cancellationTokenSource; private readonly ConcurrentQueue> pendingCallbacks; - private readonly IEventSubscriber eventSubscriber; + private readonly Lazy lazyEventSubscriber; private volatile bool disposed; + private volatile bool eventsSubscribed; - public CommandTrackedService(ISwiftlyCore swiftlyCore) + public CommandTrackedService(Lazy lazySwiftlyCore) { - this.eventSubscriber = swiftlyCore.Event; + this.lazyEventSubscriber = new Lazy(() => lazySwiftlyCore.Value.Event); pendingCallbacks = new ConcurrentQueue>(); activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); currentCommandContainer = CommandIdContainer.Empty; - - eventSubscriber.OnCommandExecuteHook += ProcessCommand; - eventSubscriber.OnConsoleOutput += ProcessOutput; + eventsSubscribed = false; StartCleanupTimer(); } @@ -145,10 +139,23 @@ private void CleanupExpiredCommands() } } + private void EnsureEventsSubscribed() + { + lock (this) + { + if (eventsSubscribed || disposed) return; + + lazyEventSubscriber.Value.OnCommandExecuteHook += ProcessCommand; + lazyEventSubscriber.Value.OnConsoleOutput += ProcessOutput; + eventsSubscribed = true; + } + } + public void EnqueueCommand(Action callback) { if (disposed || callback == null) return; + EnsureEventsSubscribed(); pendingCallbacks.Enqueue(callback); } @@ -158,8 +165,11 @@ public void Dispose() disposed = true; cancellationTokenSource.Cancel(); - eventSubscriber.OnCommandExecuteHook -= ProcessCommand; - eventSubscriber.OnConsoleOutput -= ProcessOutput; + if (lazyEventSubscriber.IsValueCreated) + { + lazyEventSubscriber.Value.OnCommandExecuteHook -= ProcessCommand; + lazyEventSubscriber.Value.OnConsoleOutput -= ProcessOutput; + } while (pendingCallbacks.TryDequeue(out _)) { } activeCommands.Clear(); diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs index e084d42d0..c66b3290d 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs @@ -1,17 +1,16 @@ using SwiftlyS2.Core.Natives; using SwiftlyS2.Shared.Services; using SwiftlyS2.Core.Modules.Engine; -using Microsoft.Extensions.DependencyInjection; namespace SwiftlyS2.Core.Services; internal class EngineService : IEngineService { - private readonly IServiceProvider serviceProvider; + private readonly CommandTrackedService commandTrackedService; - public EngineService(IServiceProvider serviceProvider) + public EngineService(CommandTrackedService commandTrackedService) { - this.serviceProvider = serviceProvider; + this.commandTrackedService = commandTrackedService; } public string ServerIP => NativeEngineHelpers.GetServerIP(); @@ -31,13 +30,8 @@ public void ExecuteCommand(string command) public void ExecuteCommandWithBuffer(string command, Action bufferCallback) { - try - { - var commandTracked = serviceProvider.GetService(); - commandTracked?.EnqueueCommand(bufferCallback); - NativeEngineHelpers.ExecuteCommand($"{command}^wb^"); - } - catch { } + commandTrackedService?.EnqueueCommand(bufferCallback); + NativeEngineHelpers.ExecuteCommand($"{command}^wb^"); } public bool IsMapValid(string map) diff --git a/managed/src/SwiftlyS2.Core/Services/StartupService.cs b/managed/src/SwiftlyS2.Core/Services/StartupService.cs index 77762f131..96ce39797 100644 --- a/managed/src/SwiftlyS2.Core/Services/StartupService.cs +++ b/managed/src/SwiftlyS2.Core/Services/StartupService.cs @@ -18,7 +18,7 @@ public StartupService(IServiceProvider provider) provider.UsePermissionManager(); provider.UsePluginManager(); provider.UseMenuService(); - provider.UseCommandTrackedService(); + // provider.UseCommandTrackedService(); // provider.UseTestService(); } From 928b3e95d0a3e45d12b813bc98b58d2fbad651da Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 10:21:17 +0800 Subject: [PATCH 7/9] style: Code cleanup --- .../SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs | 5 ----- managed/src/SwiftlyS2.Core/Services/StartupService.cs | 1 - 2 files changed, 6 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs index 4c5bbe83e..a28f404c6 100644 --- a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs +++ b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs @@ -10,9 +10,4 @@ public static IServiceCollection AddCommandTrackedService(this IServiceCollectio { return self.AddSingleton(provider => new CommandTrackedService(new Lazy(() => provider.GetRequiredService()))); } - - // public static void UseCommandTrackedService(this IServiceProvider self) - // { - // self.GetRequiredService(); - // } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Services/StartupService.cs b/managed/src/SwiftlyS2.Core/Services/StartupService.cs index 96ce39797..05ab5e3a2 100644 --- a/managed/src/SwiftlyS2.Core/Services/StartupService.cs +++ b/managed/src/SwiftlyS2.Core/Services/StartupService.cs @@ -18,7 +18,6 @@ public StartupService(IServiceProvider provider) provider.UsePermissionManager(); provider.UsePluginManager(); provider.UseMenuService(); - // provider.UseCommandTrackedService(); // provider.UseTestService(); } From 9051a98a217f11aa024f9f74cb5cb7aa94543ea5 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 18:23:26 +0800 Subject: [PATCH 8/9] refactor: Move CommandTrackedService and EngineService out of global registration --- managed/src/SwiftlyS2.Core/Bootstrap.cs | 4 ---- .../Hosting/CommandTrackedServiceInjection.cs | 13 ----------- .../Hosting/CoreCommandServiceInjection.cs | 15 ------------- .../Hosting/EngineServiceInjection.cs | 13 ----------- .../Modules/Engine/CommandTrackedService.cs | 22 +++++++++---------- .../Modules/Engine/EngineService.cs | 6 ++--- .../Modules/Plugins/SwiftlyCore.cs | 8 ++++++- .../SwiftlyS2.Core/Services/StartupService.cs | 2 -- managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs | 7 ++++-- .../Modules/Engine/ICommandTrackerService.cs | 10 +++++++++ 10 files changed, 35 insertions(+), 65 deletions(-) delete mode 100644 managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs delete mode 100644 managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs delete mode 100644 managed/src/SwiftlyS2.Core/Hosting/EngineServiceInjection.cs create mode 100644 managed/src/SwiftlyS2.Shared/Modules/Engine/ICommandTrackerService.cs diff --git a/managed/src/SwiftlyS2.Core/Bootstrap.cs b/managed/src/SwiftlyS2.Core/Bootstrap.cs index 55ae3bdcb..dcc9a9218 100644 --- a/managed/src/SwiftlyS2.Core/Bootstrap.cs +++ b/managed/src/SwiftlyS2.Core/Bootstrap.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Hosting; using SwiftlyS2.Core.Misc; using Microsoft.Extensions.Configuration; -using SwiftlyS2.Core.Modules.Engine; using SwiftlyS2.Shared.Memory; using SwiftlyS2.Shared.Services; namespace SwiftlyS2.Core; @@ -60,13 +59,10 @@ public static void Start(IntPtr nativeTable, int nativeTableSize, string basePat .AddPlayerManagerService() .AddPluginManager() .AddHookManager() - .AddEngineService() .AddTraceManagerService() - .AddCoreCommandService() .AddPermissionManager() .AddCoreHookService() .AddMenuService() - .AddCommandTrackedService() .AddSwiftlyCore(basePath); }) .Build(); diff --git a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs deleted file mode 100644 index a28f404c6..000000000 --- a/managed/src/SwiftlyS2.Core/Hosting/CommandTrackedServiceInjection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using SwiftlyS2.Shared; -using SwiftlyS2.Core.Modules.Engine; - -namespace SwiftlyS2.Core.Hosting; - -internal static class CommandTrackedServiceInjection -{ - public static IServiceCollection AddCommandTrackedService(this IServiceCollection self) - { - return self.AddSingleton(provider => new CommandTrackedService(new Lazy(() => provider.GetRequiredService()))); - } -} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs deleted file mode 100644 index f38034c50..000000000 --- a/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using SwiftlyS2.Core.Services; - -namespace SwiftlyS2.Core.Hosting; - -internal static class CoreCommandServiceInjection { - public static IServiceCollection AddCoreCommandService(this IServiceCollection services) { - services.AddSingleton(); - return services; - } - - public static void UseCoreCommandService(this IServiceProvider provider) { - provider.GetRequiredService(); - } -} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Hosting/EngineServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/EngineServiceInjection.cs deleted file mode 100644 index f408881e3..000000000 --- a/managed/src/SwiftlyS2.Core/Hosting/EngineServiceInjection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using SwiftlyS2.Core.Services; - -namespace SwiftlyS2.Core.Hosting; - -internal static class EngineServiceInjection -{ - public static IServiceCollection AddEngineService(this IServiceCollection self) - { - self.AddSingleton(); - return self; - } -} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs index 7969e3326..13a55a2b2 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/CommandTrackedService.cs @@ -3,10 +3,11 @@ using SwiftlyS2.Shared; using SwiftlyS2.Shared.Misc; using SwiftlyS2.Shared.Events; +using SwiftlyS2.Shared.Services; -namespace SwiftlyS2.Core.Modules.Engine; +namespace SwiftlyS2.Core.Services; -internal sealed class CommandTrackedService : IDisposable +internal sealed class CommandTrackedService : ICommandTrackedService, IDisposable { private sealed record CommandIdContainer(Guid Value) { @@ -24,13 +25,13 @@ private readonly record struct ExecutingCommand(Action Callback) private readonly ConcurrentDictionary activeCommands; private readonly CancellationTokenSource cancellationTokenSource; private readonly ConcurrentQueue> pendingCallbacks; - private readonly Lazy lazyEventSubscriber; + private readonly IEventSubscriber eventSubscriber; private volatile bool disposed; private volatile bool eventsSubscribed; - public CommandTrackedService(Lazy lazySwiftlyCore) + public CommandTrackedService(IEventSubscriber eventSubscriber) { - this.lazyEventSubscriber = new Lazy(() => lazySwiftlyCore.Value.Event); + this.eventSubscriber = eventSubscriber; pendingCallbacks = new ConcurrentQueue>(); activeCommands = new ConcurrentDictionary(); cancellationTokenSource = new CancellationTokenSource(); @@ -145,8 +146,8 @@ private void EnsureEventsSubscribed() { if (eventsSubscribed || disposed) return; - lazyEventSubscriber.Value.OnCommandExecuteHook += ProcessCommand; - lazyEventSubscriber.Value.OnConsoleOutput += ProcessOutput; + eventSubscriber.OnCommandExecuteHook += ProcessCommand; + eventSubscriber.OnConsoleOutput += ProcessOutput; eventsSubscribed = true; } } @@ -165,11 +166,8 @@ public void Dispose() disposed = true; cancellationTokenSource.Cancel(); - if (lazyEventSubscriber.IsValueCreated) - { - lazyEventSubscriber.Value.OnCommandExecuteHook -= ProcessCommand; - lazyEventSubscriber.Value.OnConsoleOutput -= ProcessOutput; - } + eventSubscriber.OnCommandExecuteHook -= ProcessCommand; + eventSubscriber.OnConsoleOutput -= ProcessOutput; while (pendingCallbacks.TryDequeue(out _)) { } activeCommands.Clear(); diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs index c66b3290d..1afbd767d 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs @@ -1,14 +1,14 @@ +using SwiftlyS2.Shared; using SwiftlyS2.Core.Natives; using SwiftlyS2.Shared.Services; -using SwiftlyS2.Core.Modules.Engine; namespace SwiftlyS2.Core.Services; internal class EngineService : IEngineService { - private readonly CommandTrackedService commandTrackedService; + private readonly ICommandTrackedService commandTrackedService; - public EngineService(CommandTrackedService commandTrackedService) + public EngineService(ICommandTrackedService commandTrackedService) { this.commandTrackedService = commandTrackedService; } diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs index 4279f6b8f..da443d35a 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs @@ -52,6 +52,7 @@ internal class SwiftlyCore : ISwiftlyCore, IDisposable public PluginConfigurationService Configuration { get; init; } public ILoggerFactory LoggerFactory { get; init; } public CommandService CommandService { get; init; } + public CommandTrackedService CommandTrackerService { get; init; } public ConsoleOutputService ConsoleOutputService { get; init; } public EntitySystemService EntitySystemService { get; init; } public ConVarService ConVarService { get; init; } @@ -81,19 +82,21 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata services .AddSingleton(id) .AddSingleton(this) + .AddSingleton(this) .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton(coreProvider.GetRequiredService()) - .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton(coreProvider.GetRequiredService()) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -113,6 +116,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata .AddSingleton(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()) + .AddSingleton(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()) @@ -145,6 +149,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata LoggerFactory = _ServiceProvider.GetRequiredService(); NetMessageService = _ServiceProvider.GetRequiredService(); CommandService = _ServiceProvider.GetRequiredService(); + CommandTrackerService = _ServiceProvider.GetRequiredService(); ConsoleOutputService = _ServiceProvider.GetRequiredService(); EntitySystemService = _ServiceProvider.GetRequiredService(); GameDataService = _ServiceProvider.GetRequiredService(); @@ -185,6 +190,7 @@ public void Dispose() IGameEventService ISwiftlyCore.GameEvent => GameEventService; INetMessageService ISwiftlyCore.NetMessage => NetMessageService; ICommandService ISwiftlyCore.Command => CommandService; + ICommandTrackedService ISwiftlyCore.CommandTracked => CommandTrackerService; IConsoleOutputService ISwiftlyCore.ConsoleOutput => ConsoleOutputService; IEntitySystemService ISwiftlyCore.EntitySystem => EntitySystemService; IConVarService ISwiftlyCore.ConVar => ConVarService; diff --git a/managed/src/SwiftlyS2.Core/Services/StartupService.cs b/managed/src/SwiftlyS2.Core/Services/StartupService.cs index 05ab5e3a2..da082aea7 100644 --- a/managed/src/SwiftlyS2.Core/Services/StartupService.cs +++ b/managed/src/SwiftlyS2.Core/Services/StartupService.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Hosting; using SwiftlyS2.Core.Hosting; using SwiftlyS2.Core.Misc; -using SwiftlyS2.Core.Modules.Engine; namespace SwiftlyS2.Core.Services; @@ -13,7 +12,6 @@ internal class StartupService : IHostedService public StartupService(IServiceProvider provider) { _provider = provider; - provider.UseCoreCommandService(); provider.UseCoreHookService(); provider.UsePermissionManager(); provider.UsePluginManager(); diff --git a/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs b/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs index aa4378f9e..308efa587 100644 --- a/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs +++ b/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs @@ -24,8 +24,6 @@ namespace SwiftlyS2.Shared; /// public interface ISwiftlyCore { - - /// /// Custom event subscriber. /// @@ -51,6 +49,11 @@ public interface ISwiftlyCore /// public ICommandService Command { get; } + /// + /// Command tracked service. + /// + public ICommandTrackedService CommandTracked { get; } + /// /// Console output service. /// diff --git a/managed/src/SwiftlyS2.Shared/Modules/Engine/ICommandTrackerService.cs b/managed/src/SwiftlyS2.Shared/Modules/Engine/ICommandTrackerService.cs new file mode 100644 index 000000000..46027ccf6 --- /dev/null +++ b/managed/src/SwiftlyS2.Shared/Modules/Engine/ICommandTrackerService.cs @@ -0,0 +1,10 @@ +namespace SwiftlyS2.Shared.Services; + +public interface ICommandTrackedService +{ + /// + /// Enqueues a command to be executed. + /// + /// The callback to receive the output of the command. + void EnqueueCommand(Action callback); +} \ No newline at end of file From 5249cca09c50c449fef1e5af36b3444231017f02 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 14 Oct 2025 18:35:20 +0800 Subject: [PATCH 9/9] revert: CoreCommandServiceInjection.cs --- .../Hosting/CoreCommandServiceInjection.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs diff --git a/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs b/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs new file mode 100644 index 000000000..f38034c50 --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Hosting/CoreCommandServiceInjection.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using SwiftlyS2.Core.Services; + +namespace SwiftlyS2.Core.Hosting; + +internal static class CoreCommandServiceInjection { + public static IServiceCollection AddCoreCommandService(this IServiceCollection services) { + services.AddSingleton(); + return services; + } + + public static void UseCoreCommandService(this IServiceProvider provider) { + provider.GetRequiredService(); + } +} \ No newline at end of file