diff --git a/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs new file mode 100644 index 000000000..1b46d074d --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputCallback.cs @@ -0,0 +1,75 @@ +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 new file mode 100644 index 000000000..fb6585dc0 --- /dev/null +++ b/managed/src/SwiftlyS2.Core/Modules/ConsoleOutput/ConsoleOutputService.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.Logging; +using SwiftlyS2.Core.Natives; +using SwiftlyS2.Shared.ConsoleOutput; +using SwiftlyS2.Shared.Profiler; + +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) + { + lock (_lock) + { + _callbacks.RemoveAll(callback => + { + if (callback.Guid == guid) + { + callback.Dispose(); + return true; + } + return false; + }); + } + } + + public bool IsFilterEnabled() + { + return NativeConsoleOutput.IsEnabled(); + } + + public void ToggleFilter() + { + NativeConsoleOutput.ToggleFilter(); + } + + public void ReloadFilterConfiguration() + { + NativeConsoleOutput.ReloadFilterConfiguration(); + } + + public bool NeedsFiltering(string message) + { + return NativeConsoleOutput.NeedsFiltering(message); + } + + public string GetCounterText() + { + return NativeConsoleOutput.GetCounterText(); + } + + 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/Plugins/SwiftlyCore.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs index c8ce68233..4279f6b8f 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SwiftlyS2.Core.Commands; +using SwiftlyS2.Core.ConsoleOutput; using SwiftlyS2.Core.Events; using SwiftlyS2.Core.GameEvents; using SwiftlyS2.Core.Misc; @@ -10,6 +11,7 @@ using SwiftlyS2.Shared.Events; using SwiftlyS2.Shared.GameEvents; using SwiftlyS2.Shared.Commands; +using SwiftlyS2.Shared.ConsoleOutput; using SwiftlyS2.Shared.NetMessages; using SwiftlyS2.Shared.Services; using SwiftlyS2.Core.AttributeParsers; @@ -50,6 +52,7 @@ internal class SwiftlyCore : ISwiftlyCore, IDisposable public PluginConfigurationService Configuration { get; init; } public ILoggerFactory LoggerFactory { get; init; } public CommandService CommandService { get; init; } + public ConsoleOutputService ConsoleOutputService { get; init; } public EntitySystemService EntitySystemService { get; init; } public ConVarService ConVarService { get; init; } public GameDataService GameDataService { get; init; } @@ -91,6 +94,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -109,6 +113,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()) @@ -140,6 +145,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata LoggerFactory = _ServiceProvider.GetRequiredService(); NetMessageService = _ServiceProvider.GetRequiredService(); CommandService = _ServiceProvider.GetRequiredService(); + ConsoleOutputService = _ServiceProvider.GetRequiredService(); EntitySystemService = _ServiceProvider.GetRequiredService(); GameDataService = _ServiceProvider.GetRequiredService(); PlayerManagerService = _ServiceProvider.GetRequiredService(); @@ -179,6 +185,7 @@ public void Dispose() IGameEventService ISwiftlyCore.GameEvent => GameEventService; INetMessageService ISwiftlyCore.NetMessage => NetMessageService; ICommandService ISwiftlyCore.Command => CommandService; + IConsoleOutputService ISwiftlyCore.ConsoleOutput => ConsoleOutputService; IEntitySystemService ISwiftlyCore.EntitySystem => EntitySystemService; IConVarService ISwiftlyCore.ConVar => ConVarService; IGameDataService ISwiftlyCore.GameData => GameDataService; diff --git a/managed/src/SwiftlyS2.Generated/Natives/ConsoleOutput.cs b/managed/src/SwiftlyS2.Generated/Natives/ConsoleOutput.cs new file mode 100644 index 000000000..a27a46f13 --- /dev/null +++ b/managed/src/SwiftlyS2.Generated/Natives/ConsoleOutput.cs @@ -0,0 +1,92 @@ +#pragma warning disable CS0649 +#pragma warning disable CS0169 + +using System.Buffers; +using System.Text; +using System.Threading; +using SwiftlyS2.Shared.Natives; + +namespace SwiftlyS2.Core.Natives; + +internal static class NativeConsoleOutput { + private static int _MainThreadID; + + private unsafe static delegate* unmanaged _AddConsoleListener; + + /// + /// callback should receive: string message + /// + public unsafe static ulong AddConsoleListener(nint callback) { + var ret = _AddConsoleListener(callback); + return ret; + } + + private unsafe static delegate* unmanaged _RemoveConsoleListener; + + public unsafe static void RemoveConsoleListener(ulong listenerId) { + _RemoveConsoleListener(listenerId); + } + + private unsafe static delegate* unmanaged _IsEnabled; + + /// + /// returns whether console filtering is enabled + /// + public unsafe static bool IsEnabled() { + var ret = _IsEnabled(); + return ret == 1; + } + + private unsafe static delegate* unmanaged _ToggleFilter; + + /// + /// toggles the console filter on/off + /// + public unsafe static void ToggleFilter() { + _ToggleFilter(); + } + + private unsafe static delegate* unmanaged _ReloadFilterConfiguration; + + /// + /// reloads the filter configuration from file + /// + public unsafe static void ReloadFilterConfiguration() { + _ReloadFilterConfiguration(); + } + + private unsafe static delegate* unmanaged _NeedsFiltering; + + /// + /// checks if a message needs filtering + /// + public unsafe static bool NeedsFiltering(string text) { + var pool = ArrayPool.Shared; + var textLength = Encoding.UTF8.GetByteCount(text); + var textBuffer = pool.Rent(textLength + 1); + Encoding.UTF8.GetBytes(text, textBuffer); + textBuffer[textLength] = 0; + fixed (byte* textBufferPtr = textBuffer) { + var ret = _NeedsFiltering(textBufferPtr); + pool.Return(textBuffer); + return ret == 1; + } + } + + private unsafe static delegate* unmanaged _GetCounterText; + + /// + /// gets the counter text showing how many messages were filtered + /// + public unsafe static string GetCounterText() { + var ret = _GetCounterText(null); + var pool = ArrayPool.Shared; + var retBuffer = pool.Rent(ret + 1); + fixed (byte* retBufferPtr = retBuffer) { + ret = _GetCounterText(retBufferPtr); + var retString = Encoding.UTF8.GetString(retBufferPtr, ret); + pool.Return(retBuffer); + return retString; + } + } +} \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs b/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs index 08fd89fc8..aa4378f9e 100644 --- a/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs +++ b/managed/src/SwiftlyS2.Shared/ISwiftlyCore.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using SwiftlyS2.Core.Services; using SwiftlyS2.Shared.Commands; +using SwiftlyS2.Shared.ConsoleOutput; using SwiftlyS2.Shared.Convars; using SwiftlyS2.Shared.Database; using SwiftlyS2.Shared.EntitySystem; @@ -50,6 +51,11 @@ public interface ISwiftlyCore /// public ICommandService Command { get; } + /// + /// Console output service. + /// + public IConsoleOutputService ConsoleOutput { get; } + /// /// Entity system service. /// diff --git a/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs b/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs new file mode 100644 index 000000000..6f44797c4 --- /dev/null +++ b/managed/src/SwiftlyS2.Shared/Modules/ConsoleOutput/IConsoleOutputService.cs @@ -0,0 +1,52 @@ +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); + + /// + /// Gets whether console filtering is enabled. + /// + /// True if filtering is enabled, false otherwise. + bool IsFilterEnabled(); + + /// + /// Toggles the console filter on/off. + /// + void ToggleFilter(); + + /// + /// Reloads the filter configuration from file. + /// + void ReloadFilterConfiguration(); + + /// + /// Checks if a message needs filtering. + /// + /// The message to check. + /// True if the message should be filtered, false otherwise. + bool NeedsFiltering(string message); + + /// + /// Gets the counter text showing how many messages were filtered. + /// + /// The counter text. + string GetCounterText(); +} \ No newline at end of file diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index f1476cf51..57b482681 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -38,7 +38,7 @@ public class TestPlugin : BasePlugin public TestPlugin(ISwiftlyCore core) : base(core) { - Console.WriteLine($"hello {core.Localizer["hello"]}"); + Console.WriteLine("[TestPlugin] TestPlugin constructed successfully!"); } public override void ConfigureSharedInterface(IInterfaceManager interfaceManager) @@ -59,7 +59,10 @@ public override void UseSharedInterface(IInterfaceManager interfaceManager) public override void Load(bool hotReload) { - + var consoleListenerId = Core.ConsoleOutput.RegisterConsoleOutputListener((message) => + { + Core.Logger.LogInformation($"message: {message}"); + }); Core.GameEvent.HookPre(@event => { diff --git a/natives/server/consoleoutput.native b/natives/server/consoleoutput.native new file mode 100644 index 000000000..080cd3494 --- /dev/null +++ b/natives/server/consoleoutput.native @@ -0,0 +1,9 @@ +class ConsoleOutput + +uint64 AddConsoleListener = ptr callback // callback should receive: string message +void RemoveConsoleListener = uint64 listenerId +bool IsEnabled = // returns whether console filtering is enabled +void ToggleFilter = // toggles the console filter on/off +void ReloadFilterConfiguration = // reloads the filter configuration from file +bool NeedsFiltering = string text // checks if a message needs filtering +string GetCounterText = // gets the counter text showing how many messages were filtered \ No newline at end of file diff --git a/src/api/engine/consoleoutput/consoleoutput.h b/src/api/engine/consoleoutput/consoleoutput.h index 875f734f4..3ed078c08 100644 --- a/src/api/engine/consoleoutput/consoleoutput.h +++ b/src/api/engine/consoleoutput/consoleoutput.h @@ -21,6 +21,7 @@ #include #include +#include class IConsoleOutput { diff --git a/src/scripting/server/consoleoutput.cpp b/src/scripting/server/consoleoutput.cpp new file mode 100644 index 000000000..1dce9218e --- /dev/null +++ b/src/scripting/server/consoleoutput.cpp @@ -0,0 +1,61 @@ +#include +#include + +#include + +uint64_t Bridge_ConsoleOutput_AddConsoleListener(void* callback) +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + return consoleOutput->AddConsoleListener([callback](const std::string& text) { + reinterpret_cast(callback)(text.c_str()); + }); +} + +void Bridge_ConsoleOutput_RemoveConsoleListener(uint64_t listenerId) +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + consoleOutput->RemoveConsoleListener(listenerId); +} + +bool Bridge_ConsoleOutput_IsEnabled() +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + return consoleOutput->IsEnabled(); +} + +void Bridge_ConsoleOutput_ToggleFilter() +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + consoleOutput->ToggleFilter(); +} + +void Bridge_ConsoleOutput_ReloadFilterConfiguration() +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + consoleOutput->ReloadFilterConfiguration(); +} + +bool Bridge_ConsoleOutput_NeedsFiltering(const char* text) +{ + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + return consoleOutput->NeedsFiltering(std::string(text)); +} + +int Bridge_ConsoleOutput_GetCounterText(char* out) +{ + thread_local static std::string counterText; + auto consoleOutput = g_ifaceService.FetchInterface(CONSOLEOUTPUT_INTERFACE_VERSION); + counterText = consoleOutput->GetCounterText(); + + if (out != nullptr) strcpy(out, counterText.c_str()); + + return counterText.size(); +} + +DEFINE_NATIVE("ConsoleOutput.AddConsoleListener", Bridge_ConsoleOutput_AddConsoleListener); +DEFINE_NATIVE("ConsoleOutput.RemoveConsoleListener", Bridge_ConsoleOutput_RemoveConsoleListener); +DEFINE_NATIVE("ConsoleOutput.IsEnabled", Bridge_ConsoleOutput_IsEnabled); +DEFINE_NATIVE("ConsoleOutput.ToggleFilter", Bridge_ConsoleOutput_ToggleFilter); +DEFINE_NATIVE("ConsoleOutput.ReloadFilterConfiguration", Bridge_ConsoleOutput_ReloadFilterConfiguration); +DEFINE_NATIVE("ConsoleOutput.NeedsFiltering", Bridge_ConsoleOutput_NeedsFiltering); +DEFINE_NATIVE("ConsoleOutput.GetCounterText", Bridge_ConsoleOutput_GetCounterText); \ No newline at end of file