Init
This commit is contained in:
commit
92e9893a35
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.vs/
|
||||||
|
obj/
|
||||||
|
bin/
|
||||||
|
*.user
|
36
PatMe2Mqtt.sln
Normal file
36
PatMe2Mqtt.sln
Normal 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
52
PatMe2Mqtt/Cmd.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
PatMe2Mqtt/FastReflectionHelper.cs
Normal file
135
PatMe2Mqtt/FastReflectionHelper.cs
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
59
PatMe2Mqtt/PatMe2Mqtt.csproj
Normal file
59
PatMe2Mqtt/PatMe2Mqtt.csproj
Normal 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>
|
11
PatMe2Mqtt/PatMe2Mqtt.json
Normal file
11
PatMe2Mqtt/PatMe2Mqtt.json
Normal 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"
|
||||||
|
}
|
19
PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs
Normal file
19
PatMe2Mqtt/PatMeProxyApi/EmoteCounter.cs
Normal 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))!;
|
||||||
|
}
|
||||||
|
}
|
21
PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs
Normal file
21
PatMe2Mqtt/PatMeProxyApi/EmoteCountersList.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
9
PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs
Normal file
9
PatMe2Mqtt/PatMeProxyApi/IPatMeProxy.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
264
PatMe2Mqtt/PatMeProxyApi/PatMe.cs
Normal file
264
PatMe2Mqtt/PatMeProxyApi/PatMe.cs
Normal 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
128
PatMe2Mqtt/Plugin.cs
Normal 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
106
PatMe2Mqtt/Service.cs
Normal 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
14
PatMe2Mqtt/SyncCmd.cs
Normal 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
BIN
PatMe2Mqtt/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
13
PatMe2Mqtt/packages.lock.json
Normal file
13
PatMe2Mqtt/packages.lock.json
Normal 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
5
README.md
Normal 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
BIN
preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
10
release.bat
Normal file
10
release.bat
Normal 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
|
Loading…
Reference in New Issue
Block a user