commit 92e9893a35da6d253323862416d4dba082956c05 Author: Jade Macho Date: Fri May 26 15:26:16 2023 +0200 Init 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..94d3abc --- /dev/null +++ b/PatMe2Mqtt/PatMe2Mqtt.csproj @@ -0,0 +1,59 @@ + + + + 0x0ade + + 0.1.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..889470b --- /dev/null +++ b/PatMe2Mqtt/PatMe2Mqtt.json @@ -0,0 +1,11 @@ +{ + "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" ], + "RepoUrl": "https://gitea.0x0a.de/0x0ade/PatMe2Mqtt" +} 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/images/icon.png b/PatMe2Mqtt/images/icon.png new file mode 100644 index 0000000..911e1dc Binary files /dev/null and b/PatMe2Mqtt/images/icon.png differ 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..e08e6d8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# PatMe2Mqtt +### Gluing PatMe and Ffxiv2Mqtt together +Basic plugin that pokes Ffxiv2Mqtt every time PatMe detects a pat. + +![preview of MQTTExplorer showing the ffxiv/patme/cid/emote topics](./preview.png) diff --git a/preview.png b/preview.png new file mode 100644 index 0000000..4e37209 Binary files /dev/null and b/preview.png differ diff --git a/release.bat b/release.bat new file mode 100644 index 0000000..cc00fec --- /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 .\PatMe2Mqtt\images\icon.png %dst%\ + +pause \ No newline at end of file