From 22ebb14e4017217f55eb8485513f63ed3a1ff382 Mon Sep 17 00:00:00 2001 From: Anna Date: Wed, 23 Dec 2020 03:52:19 -0500 Subject: [PATCH] fix: make plugin work on stock Dalamud Use some horrible, cursed AppDomain shit to load dependencies that break on normal Dalamud in their own environment, then do classification there instead. --- NoSoliciting.Classifier/Classifier.cs | 35 ---------- NoSoliciting.Classifier/Models.cs | 24 ------- NoSoliciting.Classifier/app.config | 15 ----- .../CursedWorkaround.cs | 31 +++++++++ .../FodyWeavers.xml | 9 +-- NoSoliciting.CursedWorkaround/Models.cs | 63 ++++++++++++++++++ .../NoSoliciting.CursedWorkaround.csproj | 20 +++--- .../costura64/CpuMathNative.dll | Bin 0 -> 38261 bytes NoSoliciting.Interface/Interface.cs | 7 ++ .../NoSoliciting.Interface.csproj | 7 ++ NoSoliciting.sln | 16 +++-- NoSoliciting/Message.cs | 20 ++++-- NoSoliciting/Ml/MlFilter.cs | 55 +++++++++------ NoSoliciting/Ml/Models.cs | 55 +-------------- NoSoliciting/NoSoliciting.csproj | 9 ++- NoSoliciting/NoSoliciting.json | 2 +- NoSoliciting/Plugin.cs | 25 ++++++- NoSoliciting/PluginUi.cs | 9 ++- 18 files changed, 220 insertions(+), 182 deletions(-) delete mode 100644 NoSoliciting.Classifier/Classifier.cs delete mode 100644 NoSoliciting.Classifier/Models.cs delete mode 100644 NoSoliciting.Classifier/app.config create mode 100644 NoSoliciting.CursedWorkaround/CursedWorkaround.cs rename {NoSoliciting.Classifier => NoSoliciting.CursedWorkaround}/FodyWeavers.xml (56%) create mode 100644 NoSoliciting.CursedWorkaround/Models.cs rename NoSoliciting.Classifier/NoSoliciting.Classifier.csproj => NoSoliciting.CursedWorkaround/NoSoliciting.CursedWorkaround.csproj (67%) create mode 100644 NoSoliciting.CursedWorkaround/costura64/CpuMathNative.dll create mode 100644 NoSoliciting.Interface/Interface.cs create mode 100644 NoSoliciting.Interface/NoSoliciting.Interface.csproj diff --git a/NoSoliciting.Classifier/Classifier.cs b/NoSoliciting.Classifier/Classifier.cs deleted file mode 100644 index 4379cd5..0000000 --- a/NoSoliciting.Classifier/Classifier.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.IO; -using System.Threading.Channels; -using Microsoft.ML; - -namespace NoSoliciting.Classifier { - public class Classifier : IDisposable { - private string ConfigPath { get; } - - private MLContext Context { get; } - private ITransformer Model { get; } - private DataViewSchema Schema { get; } - - private PredictionEngine PredictionEngine { get; } - - public Classifier(string configPath) { - this.ConfigPath = configPath; - - this.Context = new MLContext(); - this.Model = this.Context.Model.Load(Path.Combine(this.ConfigPath, "model.zip"), out var schema); - this.Schema = schema; - this.PredictionEngine = this.Context.Model.CreatePredictionEngine(this.Model, this.Schema); - } - - public string Classify(ushort channel, string message) { - var data = new MessageData(channel, message); - var pred = this.PredictionEngine.Predict(data); - return pred.Category; - } - - public void Dispose() { - this.PredictionEngine.Dispose(); - } - } -} diff --git a/NoSoliciting.Classifier/Models.cs b/NoSoliciting.Classifier/Models.cs deleted file mode 100644 index 45a2580..0000000 --- a/NoSoliciting.Classifier/Models.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.ML.Data; - -namespace NoSoliciting.Classifier { - public class MessageData { - public string? Category { get; } - - public uint Channel { get; } - - public string Message { get; } - - public MessageData(uint channel, string message) { - this.Channel = channel; - this.Message = message; - } - } - - public class MessagePrediction { - [ColumnName("PredictedLabel")] - public string Category { get; set; } = null!; - - [ColumnName("Score")] - public float[] Probabilities { get; set; } = null!; - } -} diff --git a/NoSoliciting.Classifier/app.config b/NoSoliciting.Classifier/app.config deleted file mode 100644 index 65000b1..0000000 --- a/NoSoliciting.Classifier/app.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/NoSoliciting.CursedWorkaround/CursedWorkaround.cs b/NoSoliciting.CursedWorkaround/CursedWorkaround.cs new file mode 100644 index 0000000..cc5914c --- /dev/null +++ b/NoSoliciting.CursedWorkaround/CursedWorkaround.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using Microsoft.ML; +using NoSoliciting.Interface; + +namespace NoSoliciting.CursedWorkaround { + [Serializable] + public class CursedWorkaround : MarshalByRefObject, IClassifier, IDisposable { + private MLContext Context { get; set; } = null!; + private ITransformer Model { get; set; } = null!; + private DataViewSchema Schema { get; set; } = null!; + private PredictionEngine PredictionEngine { get; set; } = null!; + + public void Initialise(byte[] data) { + this.Context = new MLContext(); + using var stream = new MemoryStream(data); + var model = this.Context.Model.Load(stream, out var schema); + this.Model = model; + this.Schema = schema; + this.PredictionEngine = this.Context.Model.CreatePredictionEngine(this.Model, this.Schema); + } + + public string Classify(ushort channel, string message) { + return this.PredictionEngine.Predict(new MessageData(channel, message)).Category; + } + + public void Dispose() { + this.PredictionEngine.Dispose(); + } + } +} diff --git a/NoSoliciting.Classifier/FodyWeavers.xml b/NoSoliciting.CursedWorkaround/FodyWeavers.xml similarity index 56% rename from NoSoliciting.Classifier/FodyWeavers.xml rename to NoSoliciting.CursedWorkaround/FodyWeavers.xml index 0d99d3c..66e34a3 100644 --- a/NoSoliciting.Classifier/FodyWeavers.xml +++ b/NoSoliciting.CursedWorkaround/FodyWeavers.xml @@ -3,14 +3,7 @@ Costura + NoSoliciting.Interface - - CpuMathNative - LdaNative - - - CpuMathNative - LdaNative - diff --git a/NoSoliciting.CursedWorkaround/Models.cs b/NoSoliciting.CursedWorkaround/Models.cs new file mode 100644 index 0000000..310e772 --- /dev/null +++ b/NoSoliciting.CursedWorkaround/Models.cs @@ -0,0 +1,63 @@ +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.ML.Data; + +namespace NoSoliciting.CursedWorkaround { + public class MessageData { + private static readonly Regex WardRegex = new Regex(@"w.{0,2}\d", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex PlotRegex = new Regex(@"p.{0,2}\d", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly string[] PlotWords = { + "plot", + "apartment", + "apt", + }; + + private static readonly Regex NumbersRegex = new Regex(@"\d{1,2}.{0,2}\d{1,2}", RegexOptions.Compiled); + + private static readonly string[] TradeWords = { + "B>", + "S>", + "buy", + "sell", + }; + + public string? Category { get; } + + public uint Channel { get; } + + public string Message { get; } + + public bool PartyFinder => this.Channel == 0; + + public bool Shout => this.Channel == 11 || this.Channel == 30; + + public bool ContainsWard => this.Message.ContainsIgnoreCase("ward") || WardRegex.IsMatch(this.Message); + + public bool ContainsPlot => PlotWords.Any(word => this.Message.ContainsIgnoreCase(word)) || PlotRegex.IsMatch(this.Message); + + public bool ContainsHousingNumbers => NumbersRegex.IsMatch(this.Message); + + public bool ContainsTradeWords => TradeWords.Any(word => this.Message.ContainsIgnoreCase(word)); + + public MessageData(uint channel, string message) { + this.Channel = channel; + this.Message = message; + } + } + + public class MessagePrediction { + [ColumnName("PredictedLabel")] + public string Category { get; set; } = null!; + + [ColumnName("Score")] + public float[] Probabilities { get; set; } = null!; + } + + public static class RmtExtensions { + public static bool ContainsIgnoreCase(this string haystack, string needle) { + return CultureInfo.InvariantCulture.CompareInfo.IndexOf(haystack, needle, CompareOptions.IgnoreCase) >= 0; + } + } +} diff --git a/NoSoliciting.Classifier/NoSoliciting.Classifier.csproj b/NoSoliciting.CursedWorkaround/NoSoliciting.CursedWorkaround.csproj similarity index 67% rename from NoSoliciting.Classifier/NoSoliciting.Classifier.csproj rename to NoSoliciting.CursedWorkaround/NoSoliciting.CursedWorkaround.csproj index 6ef0c2d..a1e4c83 100644 --- a/NoSoliciting.Classifier/NoSoliciting.Classifier.csproj +++ b/NoSoliciting.CursedWorkaround/NoSoliciting.CursedWorkaround.csproj @@ -2,16 +2,9 @@ net48 - enable 8 - - - - x64 - - - - x64 + enable + x64 @@ -25,4 +18,13 @@ + + + + + + + + + diff --git a/NoSoliciting.CursedWorkaround/costura64/CpuMathNative.dll b/NoSoliciting.CursedWorkaround/costura64/CpuMathNative.dll new file mode 100644 index 0000000000000000000000000000000000000000..c8cf7b531221b9315fc83f6757162c321a6eca53 GIT binary patch literal 38261 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 + + + net48 + + + diff --git a/NoSoliciting.sln b/NoSoliciting.sln index 84bfbaf..76b6b0b 100644 --- a/NoSoliciting.sln +++ b/NoSoliciting.sln @@ -9,7 +9,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSoliciting.Tests", "NoSoliciting.Tests\NoSoliciting.Tests.csproj", "{1962D91F-543A-4214-88FD-788BB7ACECE3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSoliciting.Classifier", "NoSoliciting.Classifier\NoSoliciting.Classifier.csproj", "{F5F1671E-8DC4-47E4-AD3E-55DA5BDF1B93}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSoliciting.CursedWorkaround", "NoSoliciting.CursedWorkaround\NoSoliciting.CursedWorkaround.csproj", "{F3238422-A9D8-4E71-9365-9EFFEC85CB59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSoliciting.Interface", "NoSoliciting.Interface\NoSoliciting.Interface.csproj", "{E88E57AB-EFB8-4F2F-93DB-F63123638C44}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,10 +27,14 @@ Global {1962D91F-543A-4214-88FD-788BB7ACECE3}.Debug|Any CPU.Build.0 = Debug|Any CPU {1962D91F-543A-4214-88FD-788BB7ACECE3}.Release|Any CPU.ActiveCfg = Release|Any CPU {1962D91F-543A-4214-88FD-788BB7ACECE3}.Release|Any CPU.Build.0 = Release|Any CPU - {F5F1671E-8DC4-47E4-AD3E-55DA5BDF1B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5F1671E-8DC4-47E4-AD3E-55DA5BDF1B93}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5F1671E-8DC4-47E4-AD3E-55DA5BDF1B93}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5F1671E-8DC4-47E4-AD3E-55DA5BDF1B93}.Release|Any CPU.Build.0 = Release|Any CPU + {F3238422-A9D8-4E71-9365-9EFFEC85CB59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3238422-A9D8-4E71-9365-9EFFEC85CB59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3238422-A9D8-4E71-9365-9EFFEC85CB59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3238422-A9D8-4E71-9365-9EFFEC85CB59}.Release|Any CPU.Build.0 = Release|Any CPU + {E88E57AB-EFB8-4F2F-93DB-F63123638C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E88E57AB-EFB8-4F2F-93DB-F63123638C44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E88E57AB-EFB8-4F2F-93DB-F63123638C44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E88E57AB-EFB8-4F2F-93DB-F63123638C44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NoSoliciting/Message.cs b/NoSoliciting/Message.cs index 47b558f..b468996 100644 --- a/NoSoliciting/Message.cs +++ b/NoSoliciting/Message.cs @@ -30,10 +30,11 @@ namespace NoSoliciting { public Message(uint defsVersion, ChatType type, string sender, string content, string? reason) : this( defsVersion, type, - new SeString(new Payload[] { new TextPayload(sender) }), - new SeString(new Payload[] { new TextPayload(content) }), + new SeString(new Payload[] {new TextPayload(sender)}), + new SeString(new Payload[] {new TextPayload(content)}), reason - ) { } + ) { + } [Serializable] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] @@ -41,7 +42,9 @@ namespace NoSoliciting { public Guid Id { get; set; } public uint DefinitionsVersion { get; set; } public DateTime Timestamp { get; set; } + public ushort Type { get; set; } + // note: cannot use byte[] because Newtonsoft thinks it's a good idea to always base64 byte[] // and I don't want to write a custom converter to overwrite their stupiditiy public List Sender { get; set; } @@ -54,7 +57,7 @@ namespace NoSoliciting { Id = this.Id, DefinitionsVersion = this.DefinitionsVersion, Timestamp = this.Timestamp, - Type = (ushort)this.ChatType, + Type = (ushort) this.ChatType, Sender = this.Sender.Encode().ToList(), Content = this.Content.Encode().ToList(), Reason = this.FilterReason, @@ -154,12 +157,17 @@ namespace NoSoliciting { public static class ChatTypeExt { private const ushort Clear7 = ~(~0 << 7); + public static byte LogKind(this ChatType type) => type switch { + ChatType.TellIncoming => (byte) ChatType.TellOutgoing, + _ => (byte) type, + }; + public static ChatType FromCode(ushort code) { - return (ChatType)(code & Clear7); + return (ChatType) (code & Clear7); } public static ChatType FromDalamud(XivChatType type) { - return FromCode((ushort)type); + return FromCode((ushort) type); } public static bool IsBattle(this ChatType type) { diff --git a/NoSoliciting/Ml/MlFilter.cs b/NoSoliciting/Ml/MlFilter.cs index 37cbc25..5ec90a6 100644 --- a/NoSoliciting/Ml/MlFilter.cs +++ b/NoSoliciting/Ml/MlFilter.cs @@ -1,12 +1,10 @@ using System; using System.IO; using System.Net; -using System.Reflection; using System.Text; using System.Threading.Tasks; using Dalamud.Plugin; -using Microsoft.ML; -using NoSoliciting.Properties; +using NoSoliciting.Interface; using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -20,29 +18,37 @@ namespace NoSoliciting.Ml { private const string Url = "http://localhost:8000/manifest.yaml"; public uint Version { get; } - private MLContext Context { get; } - private ITransformer Model { get; } - private DataViewSchema Schema { get; } - private PredictionEngine PredictionEngine { get; } + private IClassifier Classifier { get; } - private MlFilter(uint version, MLContext context, ITransformer model, DataViewSchema schema) { + private MlFilter(uint version, IClassifier classifier) { this.Version = version; - this.Context = context; - this.Model = model; - this.Schema = schema; - this.PredictionEngine = this.Context.Model.CreatePredictionEngine(this.Model, this.Schema); + this.Classifier = classifier; } + // private MLContext Context { get; } + // private ITransformer Model { get; } + // private DataViewSchema Schema { get; } + // private PredictionEngine PredictionEngine { get; } + // + // private MlFilter(uint version, MLContext context, ITransformer model, DataViewSchema schema) { + // this.Version = version; + // this.Context = context; + // this.Model = model; + // this.Schema = schema; + // this.PredictionEngine = this.Context.Model.CreatePredictionEngine(this.Model, this.Schema); + // } + public MessageCategory ClassifyMessage(ushort channel, string message) { - var data = new MessageData(channel, message); - var pred = this.PredictionEngine.Predict(data); - var category = MessageCategoryExt.FromString(pred.Category); + // var data = new MessageData(channel, message); + // var pred = this.PredictionEngine.Predict(data); + var rawCategory = this.Classifier.Classify(channel, message); + var category = MessageCategoryExt.FromString(rawCategory); if (category != null) { return (MessageCategory) category; } - PluginLog.LogWarning($"Unknown message category: {pred.Category}"); + PluginLog.LogWarning($"Unknown message category: {rawCategory}"); return MessageCategory.Normal; } @@ -72,11 +78,18 @@ namespace NoSoliciting.Ml { UpdateCachedFile(plugin, ModelName, data); UpdateCachedFile(plugin, ManifestName, Encoding.UTF8.GetBytes(manifest.Item2)); - var context = new MLContext(); - using var stream = new MemoryStream(data); - var model = context.Model.Load(stream, out var schema); + // var context = new MLContext(); + // using var stream = new MemoryStream(data); + // var model = context.Model.Load(stream, out var schema); - return new MlFilter(manifest.Item1.Version, context, model, schema); + // return new MlFilter(manifest.Item1.Version, context, model, schema); + + plugin.Classifier.Initialise(data); + + return new MlFilter( + manifest.Item1.Version, + plugin.Classifier + ); } private static async Task DownloadModel(Uri url) { @@ -150,7 +163,7 @@ namespace NoSoliciting.Ml { } public void Dispose() { - this.PredictionEngine.Dispose(); + // this.PredictionEngine.Dispose(); } } } diff --git a/NoSoliciting/Ml/Models.cs b/NoSoliciting/Ml/Models.cs index 51ba77f..9438f5c 100644 --- a/NoSoliciting/Ml/Models.cs +++ b/NoSoliciting/Ml/Models.cs @@ -4,57 +4,6 @@ using System.Text.RegularExpressions; using Microsoft.ML.Data; namespace NoSoliciting.Ml { - public class MessageData { - private static readonly Regex WardRegex = new Regex(@"w.{0,2}\d", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex PlotRegex = new Regex(@"p.{0,2}\d", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly string[] PlotWords = { - "plot", - "apartment", - "apt", - }; - - private static readonly Regex NumbersRegex = new Regex(@"\d{1,2}.{0,2}\d{1,2}", RegexOptions.Compiled); - - private static readonly string[] TradeWords = { - "B>", - "S>", - "buy", - "sell", - }; - - public string? Category { get; } - - public uint Channel { get; } - - public string Message { get; } - - public bool PartyFinder => this.Channel == 0; - - public bool Shout => this.Channel == 11 || this.Channel == 30; - - public bool ContainsWard => this.Message.ContainsIgnoreCase("ward") || WardRegex.IsMatch(this.Message); - - public bool ContainsPlot => PlotWords.Any(word => this.Message.ContainsIgnoreCase(word)) || PlotRegex.IsMatch(this.Message); - - public bool ContainsHousingNumbers => NumbersRegex.IsMatch(this.Message); - - public bool ContainsTradeWords => TradeWords.Any(word => this.Message.ContainsIgnoreCase(word)); - - public MessageData(uint channel, string message) { - this.Channel = channel; - this.Message = message; - } - } - - public class MessagePrediction { - [ColumnName("PredictedLabel")] - public string Category { get; set; } = null!; - - [ColumnName("Score")] - public float[] Probabilities { get; set; } = null!; - } - public enum MessageCategory { Trade, FreeCompany, @@ -80,13 +29,13 @@ namespace NoSoliciting.Ml { }; public static string Name(this MessageCategory category) => category switch { - MessageCategory.Trade => "Trades", + MessageCategory.Trade => "Trade ads", MessageCategory.FreeCompany => "Free Company ads", MessageCategory.Normal => "Normal messages", MessageCategory.Phishing => "Phishing messages", MessageCategory.RmtContent => "RMT (content)", MessageCategory.RmtGil => "RMT (gil)", - MessageCategory.Roleplaying => "Roleplaying", + MessageCategory.Roleplaying => "Roleplaying ads", MessageCategory.Static => "Static recruitment", _ => throw new ArgumentException("Invalid category", nameof(category)), }; diff --git a/NoSoliciting/NoSoliciting.csproj b/NoSoliciting/NoSoliciting.csproj index 58294b2..cf8b03e 100644 --- a/NoSoliciting/NoSoliciting.csproj +++ b/NoSoliciting/NoSoliciting.csproj @@ -36,7 +36,6 @@ - @@ -53,4 +52,12 @@ Resources.Designer.cs + + + PreserveNewest + + + + + diff --git a/NoSoliciting/NoSoliciting.json b/NoSoliciting/NoSoliciting.json index ed88e08..06df23f 100644 --- a/NoSoliciting/NoSoliciting.json +++ b/NoSoliciting/NoSoliciting.json @@ -1,7 +1,7 @@ { "Author": "ascclemens", "Name": "NoSoliciting", - "Description": "Hides RMT in chat and the party finder. /prmt", + "Description": "Customisable chat and Party Finder filtering. In addition to letting you filter anything from chat and PF, it comes with built-in filters for the following:\n\n- RMT (both gil and content)\n- FC ads\n- RP ads\n- Phishing messages\n- Static recruitment\n- Trade ads\n- Any PF with an item level over the max\n\nNow with experimental machine learning!", "InternalName": "NoSoliciting", "AssemblyVersion": "1.5.0", "RepoUrl": "https://git.sr.ht/~jkcclemens/NoSoliciting", diff --git a/NoSoliciting/Plugin.cs b/NoSoliciting/Plugin.cs index 5e491bb..39baeeb 100644 --- a/NoSoliciting/Plugin.cs +++ b/NoSoliciting/Plugin.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; +using NoSoliciting.Interface; using NoSoliciting.Ml; namespace NoSoliciting { @@ -33,9 +34,20 @@ namespace NoSoliciting { // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local public string AssemblyLocation { get; private set; } = Assembly.GetExecutingAssembly().Location; + private const string LibraryName = "NoSoliciting.CursedWorkaround"; + + private AppDomain InnerDomain { get; set; } = null!; + + public IClassifier Classifier { get; private set; } = null!; + public void Initialize(DalamudPluginInterface pluginInterface) { - // NOTE: THE SECRET IS TO DOWNGRADE System.Numerics.Vectors THAT'S INCLUDED WITH DALAMUD - // CRY + // FIXME: eventually this cursed workaround for old System.Numerics.Vectors should be destroyed + this.InnerDomain = AppDomain.CreateDomain(LibraryName, AppDomain.CurrentDomain.Evidence, new AppDomainSetup { + ApplicationName = LibraryName, + ConfigurationFile = $"{LibraryName}.dll.config", + ApplicationBase = Path.GetDirectoryName(this.AssemblyLocation), + }); + this.Classifier = (IClassifier) this.InnerDomain.CreateInstanceAndUnwrap(LibraryName, $"{LibraryName}.CursedWorkaround"); string path = Environment.GetEnvironmentVariable("PATH")!; string newPath = Path.GetDirectoryName(this.AssemblyLocation)!; @@ -78,7 +90,11 @@ namespace NoSoliciting { } Task.Run(async () => { this.MlFilter = await MlFilter.Load(this); }) - .ContinueWith(_ => PluginLog.Log("Machine learning model loaded")); + .ContinueWith(e => { + if (!e.IsFaulted) { + PluginLog.Log("Machine learning model loaded"); + } + }); } internal void UpdateDefinitions() { @@ -125,6 +141,9 @@ namespace NoSoliciting { this.Interface.UiBuilder.OnBuildUi -= this.Ui.Draw; this.Interface.UiBuilder.OnOpenConfigUi -= this.Ui.OpenSettings; this.Interface.CommandManager.RemoveHandler("/prmt"); + + // AppDomain.CurrentDomain.AssemblyResolve -= this.ResolveAssembly; + AppDomain.Unload(this.InnerDomain); } this._disposedValue = true; diff --git a/NoSoliciting/PluginUi.cs b/NoSoliciting/PluginUi.cs index ef73694..d7ab088 100644 --- a/NoSoliciting/PluginUi.cs +++ b/NoSoliciting/PluginUi.cs @@ -11,6 +11,7 @@ using System.Net; using System.Numerics; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Lumina.Excel.GeneratedSheets; using NoSoliciting.Ml; namespace NoSoliciting { @@ -275,7 +276,13 @@ namespace NoSoliciting { var types = this.Plugin.Config.MlFilters[category]; void DrawTypes(ChatType type) { - var name = type == ChatType.None ? "Party Finder" : type.ToString(); + string name; + if (type == ChatType.None) { + name = "Party Finder"; + } else { + var lf = this.Plugin.Interface.Data.GetExcelSheet().FirstOrDefault(lf => lf.LogKind == type.LogKind()); + name = lf?.Name?.ToString() ?? type.ToString(); + } var check = types.Contains(type); if (!ImGui.Checkbox(name, ref check)) {