From 1acaf0cee62403182b7cb9e25a43ab24bb315c0b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 21 Aug 2023 01:32:39 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 340 +++++++++++++++++++++++++++++++++ .gitmodules | 3 + ECommons | 1 + Influx.sln | 22 +++ Influx/.gitignore | 1 + Influx/AllaganToolsIPC.cs | 182 ++++++++++++++++++ Influx/Configuration.cs | 18 ++ Influx/DalamudPackager.targets | 20 ++ Influx/Influx.csproj | 67 +++++++ Influx/Influx.json | 7 + Influx/InfluxPlugin.cs | 122 ++++++++++++ 11 files changed, 783 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 ECommons create mode 100644 Influx.sln create mode 100644 Influx/.gitignore create mode 100644 Influx/AllaganToolsIPC.cs create mode 100644 Influx/Configuration.cs create mode 100644 Influx/DalamudPackager.targets create mode 100644 Influx/Influx.csproj create mode 100644 Influx/Influx.json create mode 100644 Influx/InfluxPlugin.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ce6fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e8e1845 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ECommons"] + path = ECommons + url = https://github.com/NightmareXIV/ECommons.git diff --git a/ECommons b/ECommons new file mode 160000 index 0000000..427a252 --- /dev/null +++ b/ECommons @@ -0,0 +1 @@ +Subproject commit 427a252eceaa274c48f79f1d9aa0a2c2d898cbf9 diff --git a/Influx.sln b/Influx.sln new file mode 100644 index 0000000..9168653 --- /dev/null +++ b/Influx.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Influx", "Influx\Influx.csproj", "{588F6FDE-C6DF-42D9-BD7A-941BE2EB7E44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECommons", "ECommons\ECommons\ECommons.csproj", "{D93B90D0-2071-475B-AADD-54D43BCF6A94}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {588F6FDE-C6DF-42D9-BD7A-941BE2EB7E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {588F6FDE-C6DF-42D9-BD7A-941BE2EB7E44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {588F6FDE-C6DF-42D9-BD7A-941BE2EB7E44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {588F6FDE-C6DF-42D9-BD7A-941BE2EB7E44}.Release|Any CPU.Build.0 = Release|Any CPU + {D93B90D0-2071-475B-AADD-54D43BCF6A94}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D93B90D0-2071-475B-AADD-54D43BCF6A94}.Debug|Any CPU.Build.0 = Debug|x64 + {D93B90D0-2071-475B-AADD-54D43BCF6A94}.Release|Any CPU.ActiveCfg = Release|x64 + {D93B90D0-2071-475B-AADD-54D43BCF6A94}.Release|Any CPU.Build.0 = Release|x64 + EndGlobalSection +EndGlobal diff --git a/Influx/.gitignore b/Influx/.gitignore new file mode 100644 index 0000000..9b1c8b1 --- /dev/null +++ b/Influx/.gitignore @@ -0,0 +1 @@ +/dist diff --git a/Influx/AllaganToolsIPC.cs b/Influx/AllaganToolsIPC.cs new file mode 100644 index 0000000..cfc4544 --- /dev/null +++ b/Influx/AllaganToolsIPC.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using ECommons.Reflection; + +namespace Influx; + +internal sealed class AllaganToolsIPC : IDisposable +{ + private readonly DalamudPluginInterface _pluginInterface; + private readonly ChatGui _chatGui; + private readonly ClientState _clientState; + private readonly ICallGateSubscriber? _initalized; + private readonly ICallGateSubscriber? _isInitialized; + + public CharacterMonitor Characters { get; private set; } + public InventoryMonitor Inventories { get; private set; } + + public AllaganToolsIPC(DalamudPluginInterface pluginInterface, ChatGui chatGui, ClientState clientState) + { + _pluginInterface = pluginInterface; + _chatGui = chatGui; + _clientState = clientState; + + _initalized = _pluginInterface.GetIpcSubscriber("AllaganTools.Initialized"); + _isInitialized = _pluginInterface.GetIpcSubscriber("AllaganTools.IsInitialized"); + _initalized.Subscribe(ConfigureIpc); + + ConfigureIpc(true); + } + + private void ConfigureIpc(bool initialized) + { + try + { + if (DalamudReflector.TryGetDalamudPlugin("Allagan Tools", out var it, false, true) && + _isInitialized != null && _isInitialized.InvokeFunc()) + { + var pluginService = it.GetType().Assembly.GetType("InventoryTools.PluginService")!; + + Characters = new CharacterMonitor(pluginService.GetProperty("CharacterMonitor")!.GetValue(null)!); + Inventories = new InventoryMonitor( + pluginService.GetProperty("InventoryMonitor")!.GetValue(null)!); + } + } + catch (Exception e) + { + _chatGui.PrintError(e.ToString()); + } + } + + public Dictionary CountGil() + { + var characters = Characters.All.ToDictionary(x => x.CharacterId, x => x); + return Inventories.All.ToDictionary( + x => characters[x.Value.CharacterId], + y => y.Value.GetAllItems().Where(x => x.ItemId == 1).Sum(x => x.Quantity)); + } + + public void Dispose() + { + _initalized.Unsubscribe(ConfigureIpc); + } + + public class CharacterMonitor + { + private readonly object _delegate; + private readonly MethodInfo _getPlayerCharacters; + private readonly MethodInfo _allCharacters; + + public CharacterMonitor(object @delegate) + { + _delegate = @delegate; + _getPlayerCharacters = _delegate.GetType().GetMethod("GetPlayerCharacters")!; + _allCharacters = _delegate.GetType().GetMethod("AllCharacters")!; + } + + public IEnumerable PlayerCharacters => GetCharactersInternal(_getPlayerCharacters); + public IEnumerable All => GetCharactersInternal(_allCharacters); + + private IEnumerable GetCharactersInternal(MethodInfo methodInfo) + { + return ((IEnumerable)methodInfo.Invoke(_delegate, Array.Empty())!) + .Cast() + .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) + .Select(x => new Character(x)) + .ToList(); + } + } + + public class Character + { + private readonly object _delegate; + private readonly FieldInfo _name; + + public Character(object @delegate) + { + _delegate = @delegate; + _name = _delegate.GetType().GetField("Name")!; + + CharacterId = (ulong)_delegate.GetType().GetField("CharacterId")!.GetValue(_delegate)!; + CharacterType = (CharacterType)_delegate.GetType().GetProperty("CharacterType")!.GetValue(_delegate)!; + OwnerId = (ulong)_delegate.GetType().GetField("OwnerId").GetValue(_delegate); + } + + public ulong CharacterId { get; } + public CharacterType CharacterType { get; } + public ulong OwnerId { get; } + public string Name => (string)_name.GetValue(_delegate)!; + } + + public enum CharacterType + { + Character, + Retainer, + FreeCompanyChest, + Housing, + Unknown, + } + + public sealed class InventoryMonitor + { + private readonly object _delegate; + private readonly PropertyInfo _inventories; + + public InventoryMonitor(object @delegate) + { + _delegate = @delegate; + _inventories = _delegate.GetType().GetProperty("Inventories")!; + } + + public IReadOnlyDictionary All => + ((IEnumerable)_inventories.GetValue(_delegate)!) + .Cast() + .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) + .Select(x => new Inventory(x)) + .ToDictionary(x => x.CharacterId, x => x); + } + + public sealed class Inventory + { + private readonly object _delegate; + private readonly MethodInfo _getAllInventories; + + public Inventory(object @delegate) + { + _delegate = @delegate; + _getAllInventories = _delegate.GetType().GetMethod("GetAllInventories")!; + CharacterId = (ulong)_delegate.GetType().GetProperty("CharacterId")!.GetValue(_delegate)!; + } + + public ulong CharacterId { get; } + + public IEnumerable GetAllItems() => + ((IEnumerable)_getAllInventories.Invoke(_delegate, Array.Empty())!) + .Cast() + .SelectMany(x => x.Cast()) + .Select(x => new InventoryItem(x)) + .ToList(); + } + + public sealed class InventoryItem + { + private readonly object _delegate; + + public InventoryItem(object @delegate) + { + _delegate = @delegate; + ItemId = (uint)_delegate.GetType().GetField("ItemId")!.GetValue(_delegate)!; + Quantity = (uint)_delegate.GetType().GetField("Quantity")!.GetValue(_delegate)!; + } + + public uint ItemId { get; } + public uint Quantity { get; } + } +} diff --git a/Influx/Configuration.cs b/Influx/Configuration.cs new file mode 100644 index 0000000..709ca7d --- /dev/null +++ b/Influx/Configuration.cs @@ -0,0 +1,18 @@ +using Dalamud.Configuration; + +namespace Influx; + +public sealed class Configuration : IPluginConfiguration +{ + public int Version { get; set; } = 1; + + public ServerConfiguration Server { get; set; } = new(); + + public sealed class ServerConfiguration + { + public string Server { get; set; } = "http://localhost:8086"; + public string Token { get; set; } = "xxx"; + public string Organization { get; set; } = "org"; + public string Bucket { get; set; } = "bucket"; + } +} diff --git a/Influx/DalamudPackager.targets b/Influx/DalamudPackager.targets new file mode 100644 index 0000000..37c782a --- /dev/null +++ b/Influx/DalamudPackager.targets @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/Influx/Influx.csproj b/Influx/Influx.csproj new file mode 100644 index 0000000..ee08837 --- /dev/null +++ b/Influx/Influx.csproj @@ -0,0 +1,67 @@ + + + net7.0-windows + 1.0 + 11.0 + enable + true + false + false + dist + true + true + portable + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + + + + $(DALAMUD_HOME)/ + + + + + + + + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + + + + + + + + + diff --git a/Influx/Influx.json b/Influx/Influx.json new file mode 100644 index 0000000..81e881b --- /dev/null +++ b/Influx/Influx.json @@ -0,0 +1,7 @@ +{ + "Name": "Influx", + "Author": "Liza Carvelli", + "Punchline": "Sync game stats to InfluxDB", + "Description": "Sync game stats to InfluxDB", + "RepoUrl": "https://git.carvel.li/liza/Influx" +} diff --git a/Influx/InfluxPlugin.cs b/Influx/InfluxPlugin.cs new file mode 100644 index 0000000..33cea1e --- /dev/null +++ b/Influx/InfluxPlugin.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using ECommons; +using InfluxDB.Client; +using InfluxDB.Client.Api.Domain; +using InfluxDB.Client.Writes; + +namespace Influx; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public class InfluxPlugin : IDalamudPlugin +{ + public string Name => "Influx"; + + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _configuration; + private readonly ClientState _clientState; + private readonly CommandManager _commandManager; + private readonly ChatGui _chatGui; + private readonly AllaganToolsIPC _allaganToolsIpc; + private readonly InfluxDBClient _influxClient; + private readonly Timer _timer; + + public InfluxPlugin(DalamudPluginInterface pluginInterface, ClientState clientState, + CommandManager commandManager, ChatGui chatGui) + { + ECommonsMain.Init(pluginInterface, this, Module.DalamudReflector); + + _pluginInterface = pluginInterface; + _configuration = LoadConfig(); + _clientState = clientState; + _commandManager = commandManager; + _chatGui = chatGui; + _allaganToolsIpc = new AllaganToolsIPC(pluginInterface, chatGui, clientState); + _influxClient = new InfluxDBClient(_configuration.Server.Server, _configuration.Server.Token); + + _commandManager.AddHandler("/influx", new CommandInfo(ProcessCommand)); + _timer = new Timer(_ => CountGil(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + } + + private Configuration LoadConfig() + { + var config = _pluginInterface.GetPluginConfig() as Configuration; + if (config != null) + return config; + + config = new Configuration(); + _pluginInterface.SavePluginConfig(config); + return config; + } + + private void ProcessCommand(string command, string arguments) + { + CountGil(); + } + + private void CountGil() + { + if (!_clientState.IsLoggedIn) + return; + + DateTime date = DateTime.UtcNow; + var stats = _allaganToolsIpc.CountGil(); + + Task.Run(async () => + { + List values = new(); + foreach (var (character, gil) in stats) + { + if (character.CharacterType == AllaganToolsIPC.CharacterType.Character) + { + values.Add(PointData.Measurement("currency") + .Tag("player_name", character.Name) + .Tag("type", character.CharacterType.ToString()) + .Field("gil", gil) + .Timestamp(date, WritePrecision.S)); + } + else if (character.CharacterType == AllaganToolsIPC.CharacterType.Retainer) + { + var owner = stats.Keys.First(x => x.CharacterId == character.OwnerId); + values.Add(PointData.Measurement("currency") + .Tag("player_name", owner.Name) + .Tag("type", character.CharacterType.ToString()) + .Tag("retainer_name", character.Name) + .Field("gil", gil) + .Timestamp(date, WritePrecision.S)); + } + } + + var writeApi = _influxClient.GetWriteApiAsync(); + try + { + await writeApi.WritePointsAsync( + values, + _configuration.Server.Bucket, _configuration.Server.Organization); + //_chatGui.Print($"Influx: {values.Count} points"); + } + catch (Exception e) + { + _chatGui.PrintError(e.ToString()); + } + }); + } + + public void Dispose() + { + _timer.Dispose(); + _commandManager.RemoveHandler("/influx"); + _influxClient.Dispose(); + _allaganToolsIpc.Dispose(); + + ECommonsMain.Dispose(); + } +}