From 576bb7b297a9420e0d190ffb3c0ed2158d5dc606 Mon Sep 17 00:00:00 2001 From: valarnin Date: Fri, 16 Sep 2022 01:08:13 -0400 Subject: [PATCH] Add custom log line functionality for OP and downstream plugins (#49) --- .../Integration/FFXIVCustomLogLines.cs | 183 ++++++++++++++++++ .../Integration/FFXIVRepository.cs | 54 ++++++ OverlayPlugin.Core/OverlayPlugin.Core.csproj | 4 + OverlayPlugin.Core/PluginMain.cs | 1 + OverlayPlugin.Core/Resources.Designer.cs | 23 ++- OverlayPlugin.Core/Resources.resx | 3 + .../resources/reserved_log_lines.json | 22 +++ 7 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 OverlayPlugin.Core/Integration/FFXIVCustomLogLines.cs create mode 100644 OverlayPlugin.Core/resources/reserved_log_lines.json diff --git a/OverlayPlugin.Core/Integration/FFXIVCustomLogLines.cs b/OverlayPlugin.Core/Integration/FFXIVCustomLogLines.cs new file mode 100644 index 000000000..3af9a1e3f --- /dev/null +++ b/OverlayPlugin.Core/Integration/FFXIVCustomLogLines.cs @@ -0,0 +1,183 @@ +using System; +using System.IO; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace RainbowMage.OverlayPlugin +{ + class FFXIVCustomLogLines + { + private ILogger logger; + private FFXIVRepository repository; + private Dictionary registry = new Dictionary(); + + private const uint registeredCustomLogLineID = 256; + + public FFXIVCustomLogLines(TinyIoCContainer container) + { + logger = container.Resolve(); + repository = container.Resolve(); + var main = container.Resolve(); + + var pluginDirectory = main.PluginDirectory; + + var reservedLogLinesPath = Path.Combine(pluginDirectory, "resources", "reserved_log_lines.json"); + + try + { + var jsonData = File.ReadAllText(reservedLogLinesPath); + var reservedData = JsonConvert.DeserializeObject>(jsonData); + logger.Log(LogLevel.Warning, $"Parsing {reservedData.Count} reserved log line entries."); + foreach (var reservedDataEntry in reservedData) + { + if (reservedDataEntry.Source == null || reservedDataEntry.Version == null) + { + logger.Log(LogLevel.Warning, $"Reserved log line entry missing Source or Version."); + continue; + } + if (reservedDataEntry.ID == null) + { + if (reservedDataEntry.StartID == null || reservedDataEntry.EndID == null) + { + logger.Log(LogLevel.Warning, $"Reserved log line entry missing StartID ({reservedDataEntry.StartID}) or EndID ({reservedDataEntry.EndID})."); + continue; + } + var Source = reservedDataEntry.Source; + var Version = reservedDataEntry.Version.Value; + var StartID = reservedDataEntry.StartID.Value; + var EndID = reservedDataEntry.EndID.Value; + logger.Log(LogLevel.Debug, $"Reserving log line entries {StartID}-{EndID} for Source {Source}, Version {Version}."); + for (uint ID = StartID; ID < EndID; ++ID) + { + if (registry.ContainsKey(ID)) + { + logger.Log(LogLevel.Error, $"Reserved log line entry already registered ({ID})."); + continue; + } + registry[ID] = new LogLineRegistryEntry() + { + ID = ID, + Source = Source, + Version = Version, + }; + } + } + else + { + var ID = reservedDataEntry.ID.Value; + if (registry.ContainsKey(ID)) + { + logger.Log(LogLevel.Error, $"Reserved log line entry already registered ({ID})."); + continue; + } + var Source = reservedDataEntry.Source; + var Version = reservedDataEntry.Version.Value; + logger.Log(LogLevel.Debug, $"Reserving log line entry for ID {ID}, Source {Source}, Version {Version}."); + registry[ID] = new LogLineRegistryEntry() + { + ID = ID, + Source = Source, + Version = Version, + }; + } + } + } catch(Exception ex) + { + logger.Log(LogLevel.Error, string.Format(Resources.ErrorCouldNotLoadReservedLogLines, ex)); + } + } + + public Func RegisterCustomLogLine(ILogLineRegistryEntry entry) + { + // Don't allow any attempt to write a custom log line with FFXIV_ACT_Plugin as the source. + // This prevents a downstream plugin from attempting to register e.g. `00` lines by just pretending to be FFXIV_ACT_Plugin. + if (entry.Source == "FFXIV_ACT_Plugin") + { + logger.Log(LogLevel.Warning, $"Attempted to register custom log line with reserved source."); + return null; + } + var ID = entry.ID; + if (registry.ContainsKey(ID)) + { + // Allow re-registering the handler if the ID and Source match. + // Implicitly don't allow re-registering the same handler if the Version changes to prevent log file confusion. + if (!registry[ID].Equals(entry)) + { + logger.Log(LogLevel.Warning, $"Reserved log line entry already registered ({ID})."); + return null; + } + } + // Write out that a new log line has been registered. Prevent newlines in the string input for sanity. + var Source = entry.Source.Replace("\r", "\\r").Replace("\n", "\\n"); + repository.WriteLogLineImpl(registeredCustomLogLineID, $"{ID}|{Source}|{entry.Version}"); + registry[ID] = entry; + return (line) => { + if (line.Contains("\r") || line.Contains("\n")) + { + logger.Log(LogLevel.Warning, $"Attempted to write custom log line with CR or LF with ID of {ID}"); + return false; + } + repository.WriteLogLineImpl(ID, line); + return true; + }; + } + } + + interface ILogLineRegistryEntry + { + uint ID { get; } + string Source { get; } + uint Version { get; } + } + + class LogLineRegistryEntry : ILogLineRegistryEntry + { + public uint ID { get; set; } + public string Source { get; set; } + public uint Version { get; set; } + + public override string ToString() + { + return Source + "|" + ID + "|" + Version; + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + var otherEntry = (ILogLineRegistryEntry)obj; + + return ID == otherEntry.ID && Source == otherEntry.Source; + } + + public override int GetHashCode() + { + int hash = 17; + hash = hash * 31 + ID.GetHashCode(); + hash = hash * 31 + Source.GetHashCode(); + return hash; + } + } + + internal interface IConfigReservedLogLine + { + uint? ID { get; } + uint? StartID { get; } + uint? EndID { get; } + string Source { get; } + uint? Version { get; } + } + + [JsonObject(NamingStrategyType = typeof(Newtonsoft.Json.Serialization.DefaultNamingStrategy))] + internal class ConfigReservedLogLine : IConfigReservedLogLine + { + public uint? ID { get; set; } + public uint? StartID { get; set; } + public uint? EndID { get; set; } + public string Source { get; set; } + public uint? Version { get; set; } + } +} \ No newline at end of file diff --git a/OverlayPlugin.Core/Integration/FFXIVRepository.cs b/OverlayPlugin.Core/Integration/FFXIVRepository.cs index 610d574a2..46143736e 100644 --- a/OverlayPlugin.Core/Integration/FFXIVRepository.cs +++ b/OverlayPlugin.Core/Integration/FFXIVRepository.cs @@ -7,6 +7,7 @@ using Advanced_Combat_Tracker; using FFXIV_ACT_Plugin.Common; using System.Collections.Generic; +using System.Reflection; namespace RainbowMage.OverlayPlugin { @@ -60,6 +61,8 @@ class FFXIVRepository private readonly ILogger logger; private IDataRepository repository; private IDataSubscription subscription; + private MethodInfo logOutputWriteLineFunc; + private object logOutput; public FFXIVRepository(TinyIoCContainer container) { @@ -297,6 +300,57 @@ public string GetLocaleString() } } + [MethodImpl(MethodImplOptions.NoInlining)] + internal bool WriteLogLineImpl(uint ID, string line) + { + if (logOutputWriteLineFunc == null) + { + var plugin = GetPluginData(); + if (plugin == null) return false; + var field = plugin.pluginObj.GetType().GetField("_iocContainer", BindingFlags.NonPublic | BindingFlags.Instance); + if (field == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve _iocContainer field information from FFXIV_ACT_Plugin"); + return false; + } + var iocContainer = field.GetValue(plugin.pluginObj); + if (iocContainer == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve _iocContainer field value from FFXIV_ACT_Plugin"); + return false; + } + var getServiceMethod = iocContainer.GetType().GetMethod("GetService"); + if (getServiceMethod == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve _iocContainer field value from FFXIV_ACT_Plugin"); + return false; + } + var logfileAssembly = AppDomain.CurrentDomain.GetAssemblies(). + SingleOrDefault(assembly => assembly.GetName().Name == "FFXIV_ACT_Plugin.Logfile"); + if (logfileAssembly == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve FFXIV_ACT_Plugin.Logfile assembly"); + return false; + } + logOutput = getServiceMethod.Invoke(iocContainer, new object[] { logfileAssembly.GetType("FFXIV_ACT_Plugin.Logfile.ILogOutput") }); + if (logOutput == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve LogOutput singleton from FFXIV_ACT_Plugin IOC"); + return false; + } + logOutputWriteLineFunc = logOutput.GetType().GetMethod("WriteLine"); + if (logOutputWriteLineFunc == null) + { + logger.Log(LogLevel.Error, "Unable to retrieve LogOutput singleton from FFXIV_ACT_Plugin IOC"); + return false; + } + } + + logOutputWriteLineFunc.Invoke(logOutput, new object[] { (int)ID, DateTime.Now, line }); + + return true; + } + // LogLineDelegate(uint EventType, uint Seconds, string logline); public void RegisterLogLineHandler(Action handler) { diff --git a/OverlayPlugin.Core/OverlayPlugin.Core.csproj b/OverlayPlugin.Core/OverlayPlugin.Core.csproj index ef0117ed9..5d9b6621c 100644 --- a/OverlayPlugin.Core/OverlayPlugin.Core.csproj +++ b/OverlayPlugin.Core/OverlayPlugin.Core.csproj @@ -108,6 +108,7 @@ + @@ -502,6 +503,9 @@ PreserveNewest + + PreserveNewest + diff --git a/OverlayPlugin.Core/PluginMain.cs b/OverlayPlugin.Core/PluginMain.cs index 4e53dd04a..5bc6b6da1 100644 --- a/OverlayPlugin.Core/PluginMain.cs +++ b/OverlayPlugin.Core/PluginMain.cs @@ -240,6 +240,7 @@ public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText) _container.Register(new FFXIVRepository(_container)); _container.Register(new NetworkParser(_container)); _container.Register(new TriggIntegration(_container)); + _container.Register(new FFXIVCustomLogLines(_container)); // This timer runs on the UI thread (it has to since we create UI controls) but LoadAddons() // can block for some time. We run it on the background thread to avoid blocking the UI. diff --git a/OverlayPlugin.Core/Resources.Designer.cs b/OverlayPlugin.Core/Resources.Designer.cs index 917cedfa1..71bf50f9c 100644 --- a/OverlayPlugin.Core/Resources.Designer.cs +++ b/OverlayPlugin.Core/Resources.Designer.cs @@ -96,6 +96,15 @@ internal static string ErrorCouldNotLoadPresets { } } + /// + /// Looks up a localized string similar to FFXIVCustomLogLines: Failed to load reserved log line: {0}. + /// + internal static string ErrorCouldNotLoadReservedLogLines { + get { + return ResourceManager.GetString("ErrorCouldNotLoadReservedLogLines", resourceCulture); + } + } + /// /// Looks up a localized string similar to Name must not be empty or white space only.. /// @@ -475,13 +484,13 @@ internal static string WSHandlerException { } /// - /// Looks up a localized string similar to <h1>It Works!</h1> - /// <p> - /// If any of these links are displayed with a &quot;Local:&quot; prefix and not clickable then that's because - /// those overlays display local files and web browsers don't allow web sites to link to local files. - /// </p> - /// <p> - /// You'll have to copy &amp; paste that link into your address bar. + /// Looks up a localized string similar to <h1>It Works!</h1> + /// <p> + /// If any of these links are displayed with a &quot;Local:&quot; prefix and not clickable then that's because + /// those overlays display local files and web browsers don't allow web sites to link to local files. + /// </p> + /// <p> + /// You'll have to copy &amp; paste that link into your address bar. /// </p>. /// internal static string WSIndexPage { diff --git a/OverlayPlugin.Core/Resources.resx b/OverlayPlugin.Core/Resources.resx index 38e3b211f..d73c0557a 100644 --- a/OverlayPlugin.Core/Resources.resx +++ b/OverlayPlugin.Core/Resources.resx @@ -279,6 +279,9 @@ NewOverlayDialog: Failed to load presets: {0} + + FFXIVCustomLogLines: Failed to load reserved log line: {0} + Preview This is used as the temporary name for the preview overlay. diff --git a/OverlayPlugin.Core/resources/reserved_log_lines.json b/OverlayPlugin.Core/resources/reserved_log_lines.json new file mode 100644 index 000000000..df3645591 --- /dev/null +++ b/OverlayPlugin.Core/resources/reserved_log_lines.json @@ -0,0 +1,22 @@ +[ + { + "StartID": 0, + "EndID": 255, + "Source": "FFXIV_ACT_Plugin", + "Version": 0, + "Comment": "Reserved for base FFXIV_ACT_Plugin use" + }, + { + "ID": 256, + "Source": "OverlayPlugin", + "Version": 1, + "Comment": "Line to be emitted when a new log line is registered" + }, + { + "StartID": 257, + "EndID": 512, + "Source": "OverlayPlugin", + "Version": 0, + "Comment": "Reserved for future OverlayPlugin use" + } +] \ No newline at end of file