From 423e463462c0d8cd32dd344076b9d4fd020db329 Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Fri, 26 May 2023 15:26:16 +0200 Subject: [PATCH] Init --- .gitignore | 4 + PatMe2Mqtt.sln | 36 +++ PatMe2Mqtt/Cmd.cs | 52 ++++ PatMe2Mqtt/FastReflectionHelper.cs | 135 +++++++++ PatMe2Mqtt/PatMe2Mqtt.csproj | 59 ++++ PatMe2Mqtt/PatMe2Mqtt.json | 10 + PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs | 19 ++ PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs | 21 ++ PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs | 9 + PatMe2Mqtt/PatMeProxyApi/PatMe.cs | 264 ++++++++++++++++++ PatMe2Mqtt/Plugin.cs | 128 +++++++++ PatMe2Mqtt/Service.cs | 106 +++++++ PatMe2Mqtt/SyncCmd.cs | 14 + PatMe2Mqtt/packages.lock.json | 13 + README.md | 3 + images/icon.png | Bin 0 -> 31764 bytes release.bat | 10 + 17 files changed, 883 insertions(+) create mode 100644 .gitignore create mode 100644 PatMe2Mqtt.sln create mode 100644 PatMe2Mqtt/Cmd.cs create mode 100644 PatMe2Mqtt/FastReflectionHelper.cs create mode 100644 PatMe2Mqtt/PatMe2Mqtt.csproj create mode 100644 PatMe2Mqtt/PatMe2Mqtt.json create mode 100644 PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs create mode 100644 PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs create mode 100644 PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs create mode 100644 PatMe2Mqtt/PatMeProxyApi/PatMe.cs create mode 100644 PatMe2Mqtt/Plugin.cs create mode 100644 PatMe2Mqtt/Service.cs create mode 100644 PatMe2Mqtt/SyncCmd.cs create mode 100644 PatMe2Mqtt/packages.lock.json create mode 100644 README.md create mode 100644 images/icon.png create mode 100644 release.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7990fe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vs/ +obj/ +bin/ +*.user \ No newline at end of file diff --git a/PatMe2Mqtt.sln b/PatMe2Mqtt.sln new file mode 100644 index 0000000..634ec60 --- /dev/null +++ b/PatMe2Mqtt.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32505.426 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PatMe2Mqtt", "PatMe2Mqtt\PatMe2Mqtt.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ECEDCE5-D60F-4A8A-AB33-4131F5C7371C}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/PatMe2Mqtt/Cmd.cs b/PatMe2Mqtt/Cmd.cs new file mode 100644 index 0000000..cd3b9ec --- /dev/null +++ b/PatMe2Mqtt/Cmd.cs @@ -0,0 +1,52 @@ +using Dalamud.Game.Command; +using System; + +namespace PatMe2Mqtt +{ + public abstract class Cmd : IDisposable + { + private CommandManager? _commandManager; + private string? _commandString; + + public abstract string Name { get; } + public string FullName => _commandString ?? $"/{Name}"; + + public abstract string HelpMessage { get; } + + public void Register(CommandManager commandManager) + { + if (_commandManager is not null) + { + Dispose(); + } + + _commandManager = commandManager; + _commandString = FullName; + + commandManager.AddHandler(FullName, new CommandInfo(Handle) + { + HelpMessage = HelpMessage + }); + } + + public abstract void Run(string arguments); + + public void Dispose() + { + _commandManager?.RemoveHandler(FullName); + _commandManager = null; + + GC.SuppressFinalize(this); + } + + private void Handle(string command, string arguments) + { + if (command != _commandString) + { + return; + } + + Run(arguments); + } + } +} diff --git a/PatMe2Mqtt/FastReflectionHelper.cs b/PatMe2Mqtt/FastReflectionHelper.cs new file mode 100644 index 0000000..5275133 --- /dev/null +++ b/PatMe2Mqtt/FastReflectionHelper.cs @@ -0,0 +1,135 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace MonoMod.Utils +{ + public delegate object? FastReflectionDelegate(object? target, params object?[] args); + + public static class FastReflectionHelper + { + private static readonly Type[] _dynamicMethodDelegateArgs = { typeof(object), typeof(object?[]) }; + + public static FastReflectionDelegate CreateFastDelegate(MethodBase method, bool directBoxValueAccess = true) + { + var dm = new DynamicMethod($"FastReflection<{method}>", typeof(object), _dynamicMethodDelegateArgs); + var il = dm.GetILGenerator(); + + var args = method.GetParameters(); + + var generateLocalBoxValuePtr = true; + + if (!method.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + + if (method.DeclaringType?.IsValueType ?? false) + { + il.Emit(OpCodes.Unbox_Any, method.DeclaringType); + } + } + + for (var i = 0; i < args.Length; i++) + { + var argType = args[i].ParameterType; + var argIsByRef = argType.IsByRef; + + if (argIsByRef) + { + argType = argType.GetElementType()!; + } + + var argIsValueType = argType.IsValueType; + + if (argIsByRef && argIsValueType && !directBoxValueAccess) + { + // Used later when storing back the reference to the new box in the array. + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, i); + } + + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, i); + + if (argIsByRef && !argIsValueType) + { + il.Emit(OpCodes.Ldelema, typeof(object)); + } + else + { + il.Emit(OpCodes.Ldelem_Ref); + + if (argIsValueType) + { + if (!argIsByRef || !directBoxValueAccess) + { + // if !directBoxValueAccess, create a new box if required + il.Emit(OpCodes.Unbox_Any, argType); + + if (argIsByRef) + { + // box back + il.Emit(OpCodes.Box, argType); + + // store new box value address to local 0 + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Unbox, argType); + + if (generateLocalBoxValuePtr) + { + generateLocalBoxValuePtr = false; + il.DeclareLocal(typeof(void*)); + } + + il.Emit(OpCodes.Stloc_0); + + // arr and index set up already + il.Emit(OpCodes.Stelem_Ref); + + // load address back to stack + il.Emit(OpCodes.Ldloc_0); + } + } + else + { + // if directBoxValueAccess, emit unbox (get value address) + il.Emit(OpCodes.Unbox, argType); + } + } + } + } + + if (method.IsConstructor) + { + il.Emit(OpCodes.Newobj, (ConstructorInfo) method); + } + else if (method.IsFinal || !method.IsVirtual) + { + il.Emit(OpCodes.Call, (MethodInfo) method); + } + else + { + il.Emit(OpCodes.Callvirt, (MethodInfo) method); + } + + var returnType = method.IsConstructor ? method.DeclaringType! : ((MethodInfo) method).ReturnType; + + if (returnType != typeof(void)) + { + if (returnType.IsValueType) + { + il.Emit(OpCodes.Box, returnType); + } + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Ret); + + return (FastReflectionDelegate) dm.CreateDelegate(typeof(FastReflectionDelegate)); + } + + } +} diff --git a/PatMe2Mqtt/PatMe2Mqtt.csproj b/PatMe2Mqtt/PatMe2Mqtt.csproj new file mode 100644 index 0000000..20b8836 --- /dev/null +++ b/PatMe2Mqtt/PatMe2Mqtt.csproj @@ -0,0 +1,59 @@ + + + + 0x0ade + + 1.0.0.0 + Gluing PatMe and Ffxiv2Mqtt together. + + + + + + net7.0-windows + x64 + enable + latest + true + false + false + true + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + + + + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + + diff --git a/PatMe2Mqtt/PatMe2Mqtt.json b/PatMe2Mqtt/PatMe2Mqtt.json new file mode 100644 index 0000000..cd2d582 --- /dev/null +++ b/PatMe2Mqtt/PatMe2Mqtt.json @@ -0,0 +1,10 @@ +{ + "Name": "PatMe2MQTT", + "InternalName": "patme2mqtt", + "Author": "0x0ade", + "ApplicableVersion": "any", + "DalamudApiLevel": 8, + "Punchline": "Gluing PatMe and FFXIV2MQTT together.", + "Description": "Basic plugin that pokes FFXIV2MQTT every time PatMe detects a pat.", + "Tags": [ "pat", "emote", "mqtt", "patme", "ffxiv2mqtt" ] +} diff --git a/PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs b/PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs new file mode 100644 index 0000000..e611bbf --- /dev/null +++ b/PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs @@ -0,0 +1,19 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using PatMe2Mqtt.PatMeProxyApi; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +// This could be optimized in a thousand different ways (better dynamic methods, source-gen this class), but eh. + +namespace PatMe2Mqtt.PatMeProxyApi +{ + public readonly record struct EmoteCounter(PatMe PatMe, object ProxiedValue) : IPatMeProxy + { + public string descSingular => (string) PatMe.GetValue(ProxiedValue, nameof(descSingular))!; + public string descPlural => (string) PatMe.GetValue(ProxiedValue, nameof(descPlural))!; + public uint Value => (uint) PatMe.GetValue(ProxiedValue, nameof(Value))!; + } +} diff --git a/PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs b/PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs new file mode 100644 index 0000000..e89e139 --- /dev/null +++ b/PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs @@ -0,0 +1,21 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +// This could be optimized in a thousand different ways (better dynamic methods, source-gen this class), but eh. + +namespace PatMe2Mqtt.PatMeProxyApi +{ + public readonly record struct EmoteCountersList(PatMe PatMe, object ProxiedValue) : IPatMeProxy, IEnumerable + { + public IEnumerator GetEnumerator() + { + foreach (var value in (IList) ProxiedValue) + { + yield return new EmoteCounter(PatMe, value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs b/PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs new file mode 100644 index 0000000..ce8f92d --- /dev/null +++ b/PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs @@ -0,0 +1,9 @@ +namespace PatMe2Mqtt.PatMeProxyApi +{ + public interface IPatMeProxy + { + PatMe PatMe { get; } + object ProxiedValue { get; } + bool IsValid => PatMe is not null && ProxiedValue is not null; + } +} diff --git a/PatMe2Mqtt/PatMeProxyApi/PatMe.cs b/PatMe2Mqtt/PatMeProxyApi/PatMe.cs new file mode 100644 index 0000000..eb6c6d7 --- /dev/null +++ b/PatMe2Mqtt/PatMeProxyApi/PatMe.cs @@ -0,0 +1,264 @@ +using Dalamud.Logging; +using MonoMod.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; + +namespace PatMe2Mqtt.PatMeProxyApi +{ + public class PatMe + { + internal const BindingFlags _BindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; + + private object? _realPatMe; + private Type? _realPatMeType; + private Type? _realPatMeServiceType; + + private WeakReference? _lastPatMe; + private readonly Dictionary<(Type, string), FastReflectionDelegate> _methodCache = new(); + private readonly Dictionary<(Type, string), MemberInfo> _memberCache = new(); + + public PatMe() + { + // Refresh(); + } + + public bool IsAvailable => _realPatMe is not null; + + public EmoteCountersList EmoteCounters { get; private set; } + + public bool Refresh() + { + PluginLog.LogInformation("Refreshing PatMeProxyApi"); + + if (Service.GetPluginInstance?.Invoke("PatMe") is not { } realPatMe) + { + PluginLog.LogInformation("PatMe not found"); + + Unset(); + + return false; + } + + _realPatMe = realPatMe; + _realPatMeType = realPatMe.GetType(); + _realPatMeServiceType = _realPatMeType.Assembly.GetType("PatMe.Service"); + + if (_realPatMeServiceType is null) + { + PluginLog.LogInformation("PatMe.Service not found"); + + Unset(); + + return false; + } + + PluginLog.LogInformation($"PatMe found: {_realPatMeType.Assembly.FullName}"); + + if (_lastPatMe?.Target != realPatMe) + { + PluginLog.LogInformation($"Different PatMe, clearing cache"); + + ClearCache(); + + _lastPatMe = new WeakReference(realPatMe); + } + + EmoteCounters = new(this, _realPatMeServiceType.GetField("emoteCounters")?.GetValue(null)!); + + return true; + } + + internal FastReflectionDelegate GetMethod(Type target, string name, Func? getMethod = null) + { + if (_methodCache.TryGetValue((target, name), out var fun)) + { + return fun; + } + + return _methodCache[(target, name)] = FastReflectionHelper.CreateFastDelegate(getMethod?.Invoke(target) ?? target.GetMethods().FirstOrDefault(m => m.Name == name) ?? target.GetMethod(name, _BindingFlags)!); + } + + internal object? InvokeStatic(string typeName, string name, params object?[] args) + { + if (_realPatMeType?.Assembly.GetType(typeName) is not { } type) + { + PluginLog.LogInformation($"GetStaticValue failed, type \"{typeName}\" not found"); + + return null; + } + + return GetMethod(type, name).Invoke(null, args); + } + + internal object? Invoke(object proxiedValue, string name, params object?[] args) + { + return GetMethod(proxiedValue.GetType(), name).Invoke(proxiedValue, args); + } + + internal object? GetStaticValue(string typeName, string name, params object?[] args) + { + if (_realPatMeType?.Assembly.GetType(typeName) is not { } type) + { + PluginLog.LogInformation($"GetStaticValue failed, type \"{typeName}\" not found"); + + return null; + } + + if (!_memberCache.TryGetValue((type, name), out var member)) + { + member = _memberCache[(type, name)] = type.GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + return field.GetValue(null); + } + + if (member is PropertyInfo property) + { + return property.GetValue(null, args); + } + + return InvokeStatic(typeName, name, args); + } + + internal object? GetValue(object proxiedValue, string name, params object?[] args) + { + if (!_memberCache.TryGetValue((proxiedValue.GetType(), name), out var member)) + { + member = _memberCache[(proxiedValue.GetType(), name)] = proxiedValue.GetType().GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + return field.GetValue(proxiedValue); + } + + if (member is PropertyInfo property) + { + return property.GetValue(proxiedValue, args); + } + + return Invoke(proxiedValue, name, args); + } + + internal void SetStaticValue(string typeName, string name, params object?[] args) + { + if (_realPatMeType?.Assembly.GetType(typeName) is not { } type) + { + PluginLog.LogInformation($"GetStaticValue failed, type \"{typeName}\" not found"); + + return; + } + + if (!_memberCache.TryGetValue((type, name), out var member)) + { + member = _memberCache[(type, name)] = type.GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + field.SetValue(null, args[0]); + + return; + } + + if (member is PropertyInfo property) + { + property.SetValue(null, args.First(), args.Length > 1 ? args.Skip(1).ToArray() : null); + + return; + } + + InvokeStatic(typeName, name, args); + } + + internal void SetValue(object proxiedValue, string name, params object?[] args) + { + if (!_memberCache.TryGetValue((proxiedValue.GetType(), name), out var member)) + { + member = _memberCache[(proxiedValue.GetType(), name)] = proxiedValue.GetType().GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + field.SetValue(proxiedValue, args[0]); + + return; + } + + if (member is PropertyInfo property) + { + property.SetValue(proxiedValue, args.First(), args.Length > 1 ? args.Skip(1).ToArray() : null); + + return; + } + + Invoke(proxiedValue, name, args); + } + + internal ITuple ConvertValueTuple(Type targetType, object proxiedValue) + { + var rawTuple = (ITuple) proxiedValue; + var dstItems = new object?[rawTuple.Length]; + var rawTypes = proxiedValue.GetType().GenericTypeArguments; + var dstTypes = targetType.GenericTypeArguments; + + for (var i = 0; i < rawTuple.Length; i++) + { + var raw = rawTuple[i]; + + if (dstTypes[i].IsAssignableFrom(rawTypes[i])) + { + dstItems[i] = raw; + } + else if (raw is not null) + { + var dstType = dstTypes[i]; + + var isNullable = dstType.FullName?.StartsWith("System.Nullable`1[[") ?? false; + + if (isNullable) + { + dstType = dstType.GenericTypeArguments[0]; + } + + dstItems[i] = Activator.CreateInstance(dstType, new object[] { this, raw }); + + if (isNullable) + { + dstItems[i] = Activator.CreateInstance(dstTypes[i], new object[] { dstItems[i]! }); + } + } + else + { + dstItems[i] = null; + } + } + + return (ITuple) Activator.CreateInstance(targetType, dstItems)!; + } + + private void Unset() + { + _realPatMe = default; + _realPatMeType = default; + _realPatMeServiceType = default; + + EmoteCounters = default; + + ClearCache(); + } + + private void ClearCache() + { + _lastPatMe = null; + _methodCache.Clear(); + _memberCache.Clear(); + } + } +} diff --git a/PatMe2Mqtt/Plugin.cs b/PatMe2Mqtt/Plugin.cs new file mode 100644 index 0000000..e0b95e3 --- /dev/null +++ b/PatMe2Mqtt/Plugin.cs @@ -0,0 +1,128 @@ +using Dalamud.IoC; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using PatMe2Mqtt.PatMeProxyApi; +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; + +namespace PatMe2Mqtt +{ + public sealed class Plugin : IDalamudPlugin + { + private readonly List _cmds; + private readonly ICallGateSubscriber _ipcPatMeCounter; + private readonly ICallGateSubscriber _ipcMqttPublish; + + public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) + { + pluginInterface.Create(); + + Service.Plugin = this; + Service.PatMe = new(); + + _cmds = typeof(Plugin).Assembly.GetTypes() + .Where(t => !t.IsAbstract && typeof(Cmd).IsAssignableFrom(t)) + .Select(t => (Cmd) Activator.CreateInstance(t)!) + .ToList(); + + foreach (Cmd cmd in _cmds) + { + cmd.Register(Service.CommandManager); + } + + _ipcPatMeCounter = pluginInterface.GetIpcSubscriber("patMeEmoteCounter"); + _ipcMqttPublish = pluginInterface.GetIpcSubscriber("Ffxiv2Mqtt.Publish"); + + SyncAll(); + + Service.ClientState.Login += OnLogIn; + _ipcPatMeCounter.Subscribe(OnPatMeEmote); + } + + public string Name => "PatMe2Mqtt"; + + public void Dispose() + { + Service.ClientState.Login -= OnLogIn; + _ipcPatMeCounter.Unsubscribe(OnPatMeEmote); + + foreach (Cmd cmd in _cmds) + { + cmd.Dispose(); + } + + Service.Plugin = null!; + } + + public void SyncAll() + { + try + { + if (!Service.PatMe.Refresh()) + { + return; + } + + foreach (var emoteCounter in Service.PatMe.EmoteCounters) + { + Sync(emoteCounter); + } + } + catch (Exception e) + { + PluginLog.LogError(e, "PatMe2Mqtt couldn't SyncAll"); + } + } + + private void SyncByDescSingular(string descSingular) + { + try + { + if (!Service.PatMe.Refresh()) + { + return; + } + + foreach (var emoteCounter in Service.PatMe.EmoteCounters) + { + if (emoteCounter.descSingular == descSingular) + { + Sync(emoteCounter); + } + } + } + catch (Exception e) + { + PluginLog.LogError(e, "PatMe2Mqtt couldn't SyncByDescSingular({0})", descSingular); + } + } + + private void Sync(EmoteCounter counter) + { + var topic = $"patme/{Service.ClientState.LocalContentId}/{counter.descPlural}"; + var value = counter.Value.ToString(); + + try + { + _ipcMqttPublish.InvokeFunc(topic, value); + } + catch (Exception e) + { + PluginLog.LogWarning(e, "PatMe2Mqtt couldn't Sync(\"{0}\", {1})", topic, value); + } + } + + private void OnLogIn(object? sender, EventArgs e) + { + Service.Framework.RunOnTick(SyncAll, delayTicks: 10); + } + + private void OnPatMeEmote(string descSingular, ushort emoteId, string instigatorName, uint instigatorWorld) + { + SyncByDescSingular(descSingular); + } + } +} diff --git a/PatMe2Mqtt/Service.cs b/PatMe2Mqtt/Service.cs new file mode 100644 index 0000000..3bcd976 --- /dev/null +++ b/PatMe2Mqtt/Service.cs @@ -0,0 +1,106 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.IoC; +using Dalamud.Logging; +using Dalamud.Plugin; +using System.Reflection; +using System; +using System.Collections; + +namespace PatMe2Mqtt +{ + public sealed class Service + { + public Service() + { + // THIS. IS. UGLY. + try + { + if (typeof(PluginServiceAttribute).Assembly.GetType("Dalamud.Service`1") is not { } serviceContainerContainer) + { + PluginLog.LogInformation("PenumbraCmd couldn't find the service container types."); + + return; + } + + if (typeof(PluginServiceAttribute).Assembly.GetType("Dalamud.Plugin.Internal.PluginManager") is not { } pluginManagerType) + { + PluginLog.LogInformation("PenumbraCmd couldn't find the plugin manager type."); + + return; + } + + if (typeof(PluginServiceAttribute).Assembly.GetType("Dalamud.Plugin.Internal.Types.LocalPlugin") is not { } localPluginType || + localPluginType.GetField("instance", BindingFlags.NonPublic | BindingFlags.Instance) is not { } localPluginInstanceField) + { + PluginLog.LogInformation("PenumbraCmd couldn't find the local plugin type or important members."); + + return; + } + + serviceContainerContainer = serviceContainerContainer.MakeGenericType(pluginManagerType); + + if (serviceContainerContainer.GetMethod("Get")?.Invoke(null, Array.Empty()) is not object manager) + { + PluginLog.LogInformation("PenumbraCmd couldn't obtain the plugin manager."); + + return; + } + + if (pluginManagerType.GetProperty("InstalledPlugins") is not { } installedPluginsProperty) + { + PluginLog.LogInformation("PenumbraCmd couldn't obtain the plugin list property."); + + return; + } + + GetPluginInstance = name => + { + if (installedPluginsProperty?.GetValue(manager) is not IList installedPlugins) + { + PluginLog.LogInformation("PenumbraCmd couldn't obtain the plugin list."); + + return null; + } + + foreach (var plugin in installedPlugins) + { + if (localPluginInstanceField.GetValue(plugin) is { } instance && instance.GetType().Assembly.GetName().Name == name) + { + return instance; + } + } + + return null; + }; + } + catch (Exception e) + { + PluginLog.LogInformation($"PenumbraCmd couldn't obtain the plugin manager service: {e}"); + } + } + + public static Plugin Plugin { get; internal set; } = null!; + + public static PatMeProxyApi.PatMe PatMe { get; internal set; } = null!; + + [PluginService] + public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + + [PluginService] + public static CommandManager CommandManager { get; private set; } = null!; + + [PluginService] + public static Framework Framework { get; private set; } = null!; + + [PluginService] + public static ClientState ClientState { get; private set; } = null!; + + public static Func? GetPluginInstance { get; private set; } + } +} diff --git a/PatMe2Mqtt/SyncCmd.cs b/PatMe2Mqtt/SyncCmd.cs new file mode 100644 index 0000000..cbf58e5 --- /dev/null +++ b/PatMe2Mqtt/SyncCmd.cs @@ -0,0 +1,14 @@ +namespace PatMe2Mqtt +{ + public sealed class SyncCmd : Cmd + { + public override string Name => "patme2mqtt"; + + public override string HelpMessage => "Manually pushes the PatMe stats to MQTT."; + + public override void Run(string arguments) + { + Service.Plugin.SyncAll(); + } + } +} diff --git a/PatMe2Mqtt/packages.lock.json b/PatMe2Mqtt/packages.lock.json new file mode 100644 index 0000000..467f0f2 --- /dev/null +++ b/PatMe2Mqtt/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net7.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.11, )", + "resolved": "2.1.11", + "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d377938 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# PatMe2Mqtt +### Gluing PatMe and Ffxiv2Mqtt together +Basic plugin that pokes Ffxiv2Mqtt every time PatMe detects a pat. diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..911e1dc3eb0ef3fd340d0b8d56760225238afd17 GIT binary patch literal 31764 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 literal 0 HcmV?d00001 diff --git a/release.bat b/release.bat new file mode 100644 index 0000000..a53776c --- /dev/null +++ b/release.bat @@ -0,0 +1,10 @@ +@set src=D:\Repos\ffxiv\PatMe2Mqtt\PatMe2Mqtt\bin\x64\Release\PatMe2Mqtt +@set dst=O:\home\ade\wwwext\xiv\PatMe2Mqtt + +dotnet build -c Release + +rmdir /s /q %dst% +xcopy %src%\ %dst%\ /E +xcopy .\images\icon.png %dst%\ + +pause \ No newline at end of file