This commit is contained in:
Jade Macho 2023-05-26 15:26:16 +02:00
commit 92e9893a35
Signed by: 0x0ade
GPG Key ID: 21C91DB3ADE0B6D5
18 changed files with 886 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.vs/
obj/
bin/
*.user

36
PatMe2Mqtt.sln Normal file
View File

@ -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

52
PatMe2Mqtt/Cmd.cs Normal file
View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>0x0ade</Authors>
<Company></Company>
<Version>0.1.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>

View File

@ -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"
}

View File

@ -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))!;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

128
PatMe2Mqtt/Plugin.cs Normal file
View File

@ -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);
}
}
}

106
PatMe2Mqtt/Service.cs Normal file
View File

@ -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; }
}
}

14
PatMe2Mqtt/SyncCmd.cs Normal file
View File

@ -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();
}
}
}

BIN
PatMe2Mqtt/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -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=="
}
}
}
}

5
README.md Normal file
View File

@ -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)

BIN
preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

10
release.bat Normal file
View File

@ -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