commit b2bc17078f007345809ab05b08673237ca70190c
Author: Jade Macho <nuda1998@gmail.com>
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..20b8836
--- /dev/null
+++ b/PatMe2Mqtt/PatMe2Mqtt.csproj
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <Authors>0x0ade</Authors>
+    <Company></Company>
+    <Version>1.0.0.0</Version>
+    <Description>Gluing PatMe and Ffxiv2Mqtt together.</Description>
+    <Copyright></Copyright>
+    <PackageProjectUrl></PackageProjectUrl>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <TargetFramework>net7.0-windows</TargetFramework>
+    <Platforms>x64</Platforms>
+    <Nullable>enable</Nullable>
+    <LangVersion>latest</LangVersion>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="DalamudPackager" Version="2.1.11" />
+    <Reference Include="FFXIVClientStructs">
+      <HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="Dalamud">
+      <HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="ImGui.NET">
+      <HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="ImGuiScene">
+      <HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="Lumina">
+      <HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+    <Reference Include="Lumina.Excel">
+      <HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
+      <Private>false</Private>
+    </Reference>
+  </ItemGroup>
+
+</Project>
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<EmoteCounter>
+    {
+        public IEnumerator<EmoteCounter> 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<Type, MethodBase>? 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<Cmd> _cmds;
+        private readonly ICallGateSubscriber<string, ushort, string, uint, object> _ipcPatMeCounter;
+        private readonly ICallGateSubscriber<string, string, bool> _ipcMqttPublish;
+
+        public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
+        {
+            pluginInterface.Create<Service>();
+
+            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<string, ushort, string, uint, object>("patMeEmoteCounter");
+            _ipcMqttPublish = pluginInterface.GetIpcSubscriber<string, string, bool>("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<object>()) 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<string, object?>? 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..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/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..911e1dc
Binary files /dev/null and b/images/icon.png differ
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..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