Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

41 changed files with 553 additions and 2351 deletions

@ -1 +1 @@
Subproject commit a63c8e7154e272374ffa03d5c801736d4229e38a Subproject commit 2715088db81d2be34447b27b0e5e5a3e766e47c7

@ -1 +1 @@
Subproject commit 11fd2f06e1374e846e1aada06071da5fc7ef697a Subproject commit f1c688a0599b41d70230021328a575da7351cf91

View File

@ -1,3 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Allagan/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ceruleum/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib; using LLib;
namespace Influx.AllaganTools; namespace Influx.AllaganTools;
@ -17,36 +15,26 @@ internal sealed class AllaganToolsIpc : IDisposable
private readonly DalamudReflector _dalamudReflector; private readonly DalamudReflector _dalamudReflector;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly ICallGateSubscriber<bool, bool> _initialized; private readonly ICallGateSubscriber<bool, bool>? _initalized;
private readonly ICallGateSubscriber<Dictionary<string, string>> _getSearchFilters; private readonly ICallGateSubscriber<bool>? _isInitialized;
private ICharacterMonitor _characters; public ICharacterMonitor Characters { get; private set; } = new UnavailableCharacterMonitor();
private IInventoryMonitor _inventories; public IInventoryMonitor Inventories { get; private set; } = new UnavailableInventoryMonitor();
private IListService _lists;
public AllaganToolsIpc(IDalamudPluginInterface pluginInterface, IChatGui chatGui, DalamudReflector dalamudReflector, public AllaganToolsIpc(DalamudPluginInterface pluginInterface, IChatGui chatGui, DalamudReflector dalamudReflector, IFramework framework, IPluginLog pluginLog)
IFramework framework, IPluginLog pluginLog)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_dalamudReflector = dalamudReflector; _dalamudReflector = dalamudReflector;
_framework = framework; _framework = framework;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_initialized = pluginInterface.GetIpcSubscriber<bool, bool>("AllaganTools.Initialized"); _initalized = pluginInterface.GetIpcSubscriber<bool, bool>("AllaganTools.Initialized");
_getSearchFilters = _isInitialized = pluginInterface.GetIpcSubscriber<bool>("AllaganTools.IsInitialized");
pluginInterface.GetIpcSubscriber<Dictionary<string, string>>("AllaganTools.GetSearchFilters"); _initalized.Subscribe(ConfigureIpc);
_characters = new UnavailableCharacterMonitor(_pluginLog);
_inventories = new UnavailableInventoryMonitor(_pluginLog);
_lists = new UnavailableListService(_pluginLog);
_initialized.Subscribe(ConfigureIpc);
try try
{ {
ICallGateSubscriber<bool> isInitializedFunc = bool isInitialized = _isInitialized.InvokeFunc();
pluginInterface.GetIpcSubscriber<bool>("AllaganTools.IsInitialized");
bool isInitialized = isInitializedFunc.InvokeFunc();
if (isInitialized) if (isInitialized)
ConfigureIpc(true); ConfigureIpc(true);
} }
@ -65,25 +53,11 @@ internal sealed class AllaganToolsIpc : IDisposable
{ {
if (_dalamudReflector.TryGetDalamudPlugin("Allagan Tools", out var it, false, true)) if (_dalamudReflector.TryGetDalamudPlugin("Allagan Tools", out var it, false, true))
{ {
var hostedPlugin = it.GetType().BaseType!; var pluginService = it.GetType().Assembly.GetType("InventoryTools.PluginService")!;
var host = hostedPlugin.GetField("host", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(it)!;
var serviceProvider = host.GetType().GetProperty("Services")!.GetValue(host)!;
var getServiceMethod = serviceProvider.GetType().GetMethod("GetService")!;
object GetService(Type t) => getServiceMethod.Invoke(serviceProvider, [t])!;
var ccl = it.GetType() Characters = new CharacterMonitor(pluginService.GetProperty("CharacterMonitor")!.GetValue(null)!);
.GetField("_service", BindingFlags.NonPublic | BindingFlags.Instance)! Inventories = new InventoryMonitor(
.GetValue(it)! pluginService.GetProperty("InventoryMonitor")!.GetValue(null)!);
.GetType()
.Assembly;
_characters =
new CharacterMonitor(GetService(ccl.GetType("CriticalCommonLib.Services.ICharacterMonitor")!));
_inventories = new InventoryMonitor(
GetService(ccl.GetType("CriticalCommonLib.Services.IInventoryMonitor")!));
_lists = new ListService(
GetService(it.GetType().Assembly.GetType("InventoryTools.Services.Interfaces.IListService")!),
GetService(it.GetType().Assembly.GetType("InventoryTools.Lists.ListFilterService")!));
} }
else else
{ {
@ -98,37 +72,11 @@ internal sealed class AllaganToolsIpc : IDisposable
}, TimeSpan.FromMilliseconds(100)); }, TimeSpan.FromMilliseconds(100));
} }
public Dictionary<string, string> GetSearchFilters()
{
try
{
return _getSearchFilters.InvokeFunc();
}
catch (IpcError e)
{
_pluginLog.Error(e, "Unable to retrieve allagantools filters");
return new Dictionary<string, string>();
}
}
public FilterResult? GetFilter(string keyOrName)
{
try
{
return _lists.GetFilterByKeyOrName(keyOrName);
}
catch (IpcError e)
{
_pluginLog.Error(e, $"Unable to retrieve filter items for filter '{keyOrName}'");
return null;
}
}
public Dictionary<Character, Currencies> CountCurrencies() public Dictionary<Character, Currencies> CountCurrencies()
{ {
_pluginLog.Verbose($"Updating characters with {_characters.GetType()} and {_inventories.GetType()}"); _pluginLog.Debug($"{Characters.GetType()}, {Inventories.GetType()}");
var characters = _characters.All.ToDictionary(x => x.CharacterId, x => x); var characters = Characters.All.ToDictionary(x => x.CharacterId, x => x);
return _inventories.All return Inventories.All
.Where(x => characters.ContainsKey(x.Value.CharacterId)) .Where(x => characters.ContainsKey(x.Value.CharacterId))
.ToDictionary( .ToDictionary(
x => characters[x.Value.CharacterId], x => characters[x.Value.CharacterId],
@ -141,26 +89,30 @@ internal sealed class AllaganToolsIpc : IDisposable
GcSealsMaelstrom = inv.Sum(20), GcSealsMaelstrom = inv.Sum(20),
GcSealsTwinAdders = inv.Sum(21), GcSealsTwinAdders = inv.Sum(21),
GcSealsImmortalFlames = inv.Sum(22), GcSealsImmortalFlames = inv.Sum(22),
FcCredits = inv.Sum(80),
Ventures = inv.Sum(21072), Ventures = inv.Sum(21072),
CeruleumTanks = inv.Sum(10155), CeruleumTanks = inv.Sum(10155),
RepairKits = inv.Sum(10373), RepairKits = inv.Sum(10373),
FreeSlots = inv.FreeInventorySlots,
}; };
}); });
} }
public void Dispose() public void Dispose()
{ {
_initialized.Unsubscribe(ConfigureIpc); _initalized?.Unsubscribe(ConfigureIpc);
_characters = new UnavailableCharacterMonitor(_pluginLog); Characters = new UnavailableCharacterMonitor();
_inventories = new UnavailableInventoryMonitor(_pluginLog); Inventories = new UnavailableInventoryMonitor();
_lists = new UnavailableListService(_pluginLog);
} }
private sealed class InventoryWrapper(IEnumerable<InventoryItem> items) private sealed class InventoryWrapper
{ {
public long Sum(int itemId) => items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity); private readonly IEnumerable<InventoryItem> _items;
public int FreeInventorySlots => 140 - items.Count(x => x.Category == 1); public InventoryWrapper(IEnumerable<InventoryItem> items)
{
_items = items;
}
public long Sum(int itemId) => _items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity);
} }
} }

View File

@ -1,4 +1,3 @@
using System;
using System.Reflection; using System.Reflection;
namespace Influx.AllaganTools; namespace Influx.AllaganTools;
@ -11,7 +10,6 @@ internal sealed class Character
public Character(object @delegate) public Character(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate);
_delegate = @delegate; _delegate = @delegate;
_name = _delegate.GetType().GetField("Name")!; _name = _delegate.GetType().GetField("Name")!;
_level = _delegate.GetType().GetField("Level")!; _level = _delegate.GetType().GetField("Level")!;
@ -21,18 +19,13 @@ internal sealed class Character
ClassJob = (byte)_delegate.GetType().GetField("ClassJob")!.GetValue(_delegate)!; ClassJob = (byte)_delegate.GetType().GetField("ClassJob")!.GetValue(_delegate)!;
OwnerId = (ulong)_delegate.GetType().GetField("OwnerId")!.GetValue(_delegate)!; OwnerId = (ulong)_delegate.GetType().GetField("OwnerId")!.GetValue(_delegate)!;
FreeCompanyId = (ulong)_delegate.GetType().GetField("FreeCompanyId")!.GetValue(_delegate)!; FreeCompanyId = (ulong)_delegate.GetType().GetField("FreeCompanyId")!.GetValue(_delegate)!;
WorldId = (uint)_delegate.GetType().GetField("WorldId")!.GetValue(_delegate)!;
} }
public ulong CharacterId { get; } public ulong CharacterId { get; }
public CharacterType CharacterType { get; } public CharacterType CharacterType { get; }
public byte ClassJob { get; } public byte ClassJob { get; }
public ulong OwnerId { get; } public ulong OwnerId { get; }
public ulong FreeCompanyId { get; set; } public ulong FreeCompanyId { get; }
public uint WorldId { get; }
public string Name => (string)_name.GetValue(_delegate)!; public string Name => (string)_name.GetValue(_delegate)!;
public uint Level => (uint)_level.GetValue(_delegate)!; public uint Level => (uint)_level.GetValue(_delegate)!;
public override string ToString() =>
$"{nameof(Character)}[{CharacterId}, {(CharacterType == CharacterType.FreeCompanyChest ? "FC" : CharacterType)}, {Name}, {WorldId}]";
} }

View File

@ -14,17 +14,15 @@ internal sealed class CharacterMonitor : ICharacterMonitor
public CharacterMonitor(object @delegate) public CharacterMonitor(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate);
_delegate = @delegate; _delegate = @delegate;
_getPlayerCharacters = _getPlayerCharacters = _delegate.GetType().GetMethod("GetPlayerCharacters")!;
_delegate.GetType().GetMethod("GetPlayerCharacters") ?? throw new MissingMethodException(); _allCharacters = _delegate.GetType().GetMethod("AllCharacters")!;
_allCharacters = _delegate.GetType().GetMethod("AllCharacters") ?? throw new MissingMethodException();
} }
public IEnumerable<Character> PlayerCharacters => GetCharactersInternal(_getPlayerCharacters); public IEnumerable<Character> PlayerCharacters => GetCharactersInternal(_getPlayerCharacters);
public IEnumerable<Character> All => GetCharactersInternal(_allCharacters); public IEnumerable<Character> All => GetCharactersInternal(_allCharacters);
private List<Character> GetCharactersInternal(MethodInfo methodInfo) private IEnumerable<Character> GetCharactersInternal(MethodInfo methodInfo)
{ {
return ((IEnumerable)methodInfo.Invoke(_delegate, Array.Empty<object>())!) return ((IEnumerable)methodInfo.Invoke(_delegate, Array.Empty<object>())!)
.Cast<object>() .Cast<object>()

View File

@ -10,5 +10,4 @@ internal struct Currencies
public long Ventures { get; init; } public long Ventures { get; init; }
public long CeruleumTanks { get; init; } public long CeruleumTanks { get; init; }
public long RepairKits { get; init; } public long RepairKits { get; init; }
public int FreeSlots { get; init; }
} }

View File

@ -1,29 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Influx.AllaganTools;
internal sealed class FilterResult
{
private readonly IEnumerable _searchResultList;
public FilterResult(IEnumerable searchResultList)
{
ArgumentNullException.ThrowIfNull(searchResultList);
_searchResultList = searchResultList;
}
public IReadOnlyList<SortingResult> GenerateFilteredList()
{
return _searchResultList
.Cast<object>()
.Select(x => x.GetType()
.GetField("_sortingResult", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(x)!)
.Select(x => new SortingResult(x))
.ToList();
}
}

View File

@ -1,6 +0,0 @@
namespace Influx.AllaganTools;
internal interface IListService
{
FilterResult? GetFilterByKeyOrName(string keyOrName);
}

View File

@ -13,9 +13,8 @@ internal sealed class Inventory
public Inventory(object @delegate) public Inventory(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate);
_delegate = @delegate; _delegate = @delegate;
_getAllInventories = _delegate.GetType().GetMethod("GetAllInventories") ?? throw new MissingMethodException(); _getAllInventories = _delegate.GetType().GetMethod("GetAllInventories")!;
CharacterId = (ulong)_delegate.GetType().GetProperty("CharacterId")!.GetValue(_delegate)!; CharacterId = (ulong)_delegate.GetType().GetProperty("CharacterId")!.GetValue(_delegate)!;
} }
@ -27,6 +26,5 @@ internal sealed class Inventory
.SelectMany(x => x.Cast<object?>()) .SelectMany(x => x.Cast<object?>())
.Where(x => x != null) .Where(x => x != null)
.Select(x => new InventoryItem(x!)) .Select(x => new InventoryItem(x!))
.Where(x => x.ItemId != 0)
.ToList(); .ToList();
} }

View File

@ -1,21 +1,16 @@
using System; namespace Influx.AllaganTools;
using Dalamud.Logging;
namespace Influx.AllaganTools;
internal sealed class InventoryItem internal sealed class InventoryItem
{ {
private readonly object _delegate;
public InventoryItem(object @delegate) public InventoryItem(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate); _delegate = @delegate;
Category = (int)@delegate.GetType().GetField("SortedCategory")!.GetValue(@delegate)!; ItemId = (uint)_delegate.GetType().GetField("ItemId")!.GetValue(_delegate)!;
Container = (int)@delegate.GetType().GetField("SortedContainer")!.GetValue(@delegate)!; Quantity = (uint)_delegate.GetType().GetField("Quantity")!.GetValue(_delegate)!;
ItemId = (uint)@delegate.GetType().GetProperty("ItemId")!.GetValue(@delegate)!;
Quantity = (uint)@delegate.GetType().GetField("Quantity")!.GetValue(@delegate)!;
} }
public int Category { get; }
public int Container { get; }
public uint ItemId { get; } public uint ItemId { get; }
public uint Quantity { get; } public uint Quantity { get; }
} }

View File

@ -1,5 +1,4 @@
using System; using System.Collections;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -13,7 +12,6 @@ internal sealed class InventoryMonitor : IInventoryMonitor
public InventoryMonitor(object @delegate) public InventoryMonitor(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate);
_delegate = @delegate; _delegate = @delegate;
_inventories = _delegate.GetType().GetProperty("Inventories")!; _inventories = _delegate.GetType().GetProperty("Inventories")!;
} }

View File

@ -1,29 +0,0 @@
using System;
using System.Collections;
using System.Reflection;
namespace Influx.AllaganTools;
internal sealed class ListService : IListService
{
private readonly object _listService;
private readonly object _listFilterService;
private readonly MethodInfo _getListByKeyOrName;
private readonly MethodInfo _refreshList;
public ListService(object listService, object listFilterService)
{
ArgumentNullException.ThrowIfNull(listService);
_listService = listService;
_listFilterService = listFilterService;
_getListByKeyOrName =
_listService.GetType().GetMethod("GetListByKeyOrName") ?? throw new MissingMethodException();
_refreshList = _listFilterService.GetType().GetMethod("RefreshList") ?? throw new MissingMethodException();
}
public FilterResult? GetFilterByKeyOrName(string keyOrName)
{
var f = _getListByKeyOrName.Invoke(_listService, [keyOrName]);
return f != null ? new FilterResult((IEnumerable)_refreshList.Invoke(_listFilterService, [f])!) : null;
}
}

View File

@ -1,31 +0,0 @@
using System;
namespace Influx.AllaganTools;
using ItemFlags = FFXIVClientStructs.FFXIV.Client.Game.InventoryItem.ItemFlags;
internal sealed class SortingResult
{
public SortingResult(object @delegate)
{
ArgumentNullException.ThrowIfNull(@delegate);
LocalContentId = (ulong)@delegate.GetType().GetProperty("SourceRetainerId")!.GetValue(@delegate)!;
Quantity = (int)@delegate.GetType().GetProperty("Quantity")!.GetValue(@delegate)!;
var inventoryItem = @delegate.GetType().GetProperty("InventoryItem")!.GetValue(@delegate)!;
ItemId = (uint)inventoryItem.GetType().GetProperty("ItemId")!.GetValue(inventoryItem)!;
Flags = (ItemFlags)inventoryItem.GetType().GetField("Flags")!.GetValue(inventoryItem)!;
}
public ulong LocalContentId { get; }
public uint ItemId { get; }
public ItemFlags Flags { get; }
public int Quantity { get; }
public bool IsHq => Flags.HasFlag(ItemFlags.HighQuality);
public override string ToString()
{
return $"{LocalContentId}, {ItemId}, {Quantity}";
}
}

View File

@ -1,26 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Plugin.Services;
namespace Influx.AllaganTools; namespace Influx.AllaganTools;
internal sealed class UnavailableCharacterMonitor(IPluginLog pluginLog) : ICharacterMonitor internal sealed class UnavailableCharacterMonitor : ICharacterMonitor
{ {
public IEnumerable<Character> PlayerCharacters public IEnumerable<Character> PlayerCharacters => Array.Empty<Character>();
{ public IEnumerable<Character> All => Array.Empty<Character>();
get
{
pluginLog.Warning("Character monitor is unavailable");
return Array.Empty<Character>();
}
}
public IEnumerable<Character> All
{
get
{
pluginLog.Warning("Character monitor is unavailable");
return Array.Empty<Character>();
}
}
} }

View File

@ -1,16 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Plugin.Services;
namespace Influx.AllaganTools; namespace Influx.AllaganTools;
internal sealed class UnavailableInventoryMonitor(IPluginLog pluginLog) : IInventoryMonitor internal sealed class UnavailableInventoryMonitor : IInventoryMonitor
{ {
public IReadOnlyDictionary<ulong, Inventory> All public IReadOnlyDictionary<ulong, Inventory> All => new Dictionary<ulong, Inventory>();
{
get
{
pluginLog.Warning("Inventory monitor is unavailable");
return new Dictionary<ulong, Inventory>();
}
}
} }

View File

@ -1,12 +0,0 @@
using Dalamud.Plugin.Services;
namespace Influx.AllaganTools;
internal sealed class UnavailableListService(IPluginLog pluginLog) : IListService
{
public FilterResult? GetFilterByKeyOrName(string keyOrName)
{
pluginLog.Warning("Filter Service is unavailable");
return null;
}
}

View File

@ -3,22 +3,21 @@ using Dalamud.Configuration;
namespace Influx; namespace Influx;
internal sealed class Configuration : IPluginConfiguration public sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public ServerConfiguration Server { get; set; } = new(); public ServerConfiguration Server { get; set; } = new();
public IList<CharacterInfo> IncludedCharacters { get; set; } = new List<CharacterInfo>(); public List<CharacterInfo> IncludedCharacters { get; set; } = new();
public IList<FilterInfo> IncludedInventoryFilters { get; set; } = new List<FilterInfo>();
public sealed class ServerConfiguration public sealed class ServerConfiguration
{ {
public bool Enabled { get; set; } public bool Enabled { get; set; }
public string Server { get; set; } = "http://localhost:8086"; public string Server { get; set; } = "http://localhost:8086";
public string Token { get; set; } = ""; public string Token { get; set; } = "xxx";
public string Organization { get; set; } = ""; public string Organization { get; set; } = "org";
public string Bucket { get; set; } = ""; public string Bucket { get; set; } = "bucket";
} }
public sealed class CharacterInfo public sealed class CharacterInfo
@ -26,11 +25,5 @@ internal sealed class Configuration : IPluginConfiguration
public ulong LocalContentId { get; set; } public ulong LocalContentId { get; set; }
public string? CachedPlayerName { get; set; } public string? CachedPlayerName { get; set; }
public string? CachedWorldName { get; set; } public string? CachedWorldName { get; set; }
public bool IncludeFreeCompany { get; set; } = true;
}
public sealed class FilterInfo
{
public required string Name { get; set; }
} }
} }

20
Influx/GameStrings.cs Normal file
View File

@ -0,0 +1,20 @@
using System;
using Dalamud.Plugin.Services;
using LLib;
using Addon = Lumina.Excel.GeneratedSheets.Addon;
namespace Influx;
internal sealed class GameStrings
{
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
LogoutToTitleScreen = dataManager.GetString<Addon>(115, s => s.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(LogoutToTitleScreen)}");
LogoutAndExitGame = dataManager.GetString<Addon>(116, s => s.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(LogoutAndExitGame)}");
}
public string LogoutToTitleScreen { get; }
public string LogoutAndExitGame { get; }
}

View File

@ -1,15 +1,57 @@
<Project Sdk="Dalamud.NET.Sdk/10.0.0"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Version>1.3</Version> <TargetFramework>net7.0-windows</TargetFramework>
<Version>0.1</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
</PropertyGroup> </PropertyGroup>
<Import Project="..\LLib\LLib.targets"/> <PropertyGroup>
<Import Project="..\LLib\RenameZip.targets"/> <DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="InfluxDB.Client" Version="4.14.0" /> <PackageReference Include="DalamudPackager" Version="2.1.12"/>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" /> <PackageReference Include="InfluxDB.Client" Version="4.13.0"/>
</ItemGroup>
<ItemGroup>
<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="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -17,4 +59,8 @@
<ProjectReference Include="..\ECommons\ECommons\ECommons.csproj" /> <ProjectReference Include="..\ECommons\ECommons\ECommons.csproj" />
<ProjectReference Include="..\LLib\LLib.csproj" /> <ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project> </Project>

View File

@ -3,6 +3,5 @@
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Sync game stats to InfluxDB", "Punchline": "Sync game stats to InfluxDB",
"Description": "Sync game stats to InfluxDB", "Description": "Sync game stats to InfluxDB",
"RepoUrl": "https://git.carvel.li/liza/Influx", "RepoUrl": "https://git.carvel.li/liza/Influx"
"IconUrl": "https://plugins.carvel.li/icons/Influx.png"
} }

View File

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -17,63 +15,33 @@ namespace Influx.Influx;
internal sealed class InfluxStatisticsClient : IDisposable internal sealed class InfluxStatisticsClient : IDisposable
{ {
private InfluxDBClient? _influxClient; private readonly InfluxDBClient _influxClient;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly IReadOnlyDictionary<byte, byte> _classJobToArrayIndex; private readonly IReadOnlyDictionary<byte, byte> _classJobToArrayIndex;
private readonly IReadOnlyDictionary<byte, string> _classJobNames; private readonly IReadOnlyDictionary<byte, string> _classJobNames;
private readonly IReadOnlyDictionary<sbyte, ClassJobDetail> _expToJobs; private readonly Dictionary<sbyte, string> _expToJobs;
private readonly ReadOnlyDictionary<uint, PriceInfo> _prices;
private readonly ReadOnlyDictionary<uint, string> _worldNames;
public InfluxStatisticsClient(IChatGui chatGui, Configuration configuration, IDataManager dataManager, public InfluxStatisticsClient(IChatGui chatGui, Configuration configuration, IDataManager dataManager,
IClientState clientState, IPluginLog pluginLog) IClientState clientState)
{ {
_influxClient = new InfluxDBClient(configuration.Server.Server, configuration.Server.Token);
_chatGui = chatGui; _chatGui = chatGui;
_configuration = configuration; _configuration = configuration;
_clientState = clientState; _clientState = clientState;
_pluginLog = pluginLog;
UpdateClient();
_classJobToArrayIndex = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0) _classJobToArrayIndex = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0)
.ToDictionary(x => (byte)x.RowId, x => (byte)x.ExpArrayIndex); .ToDictionary(x => (byte)x.RowId, x => (byte)x.ExpArrayIndex);
_classJobNames = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0) _classJobNames = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0)
.ToDictionary(x => (byte)x.RowId, x => x.Abbreviation.ToString()); .ToDictionary(x => (byte)x.RowId, x => x.Abbreviation.ToString());
_expToJobs = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0 && !string.IsNullOrEmpty(x.Name)) _expToJobs = dataManager.GetExcelSheet<ClassJob>()!.Where(x => x.RowId > 0)
.Where(x => x.JobIndex > 0 || x.DohDolJobIndex >= 0) .Where(x => x.JobIndex > 0)
.Where(x => x.Abbreviation.ToString() != "SMN") .Where(x => x.Abbreviation.ToString() != "SMN")
.ToDictionary(x => x.ExpArrayIndex, x => new ClassJobDetail(x.Abbreviation.ToString(), x.DohDolJobIndex >= 0)); .ToDictionary(x => x.ExpArrayIndex, x => x.Abbreviation.ToString());
_prices = dataManager.GetExcelSheet<Item>()!
.AsEnumerable()
.ToDictionary(x => x.RowId, x => new PriceInfo
{
Name = x.Name.ToString(),
Normal = x.PriceLow,
UiCategory = x.ItemUICategory.Row,
})
.AsReadOnly();
_worldNames = dataManager.GetExcelSheet<World>()!
.Where(x => x.RowId > 0 && x.IsPublic)
.ToDictionary(x => x.RowId, x => x.Name.ToString())
.AsReadOnly();
} }
public bool Enabled => _configuration.Server.Enabled && public bool Enabled => _configuration.Server.Enabled;
!string.IsNullOrEmpty(_configuration.Server.Server) &&
!string.IsNullOrEmpty(_configuration.Server.Token) &&
!string.IsNullOrEmpty(_configuration.Server.Organization) &&
!string.IsNullOrEmpty(_configuration.Server.Bucket);
public void UpdateClient()
{
_influxClient?.Dispose();
_influxClient = null;
if (Enabled)
_influxClient = new InfluxDBClient(_configuration.Server.Server, _configuration.Server.Token);
}
public void OnStatisticsUpdate(StatisticsUpdate update) public void OnStatisticsUpdate(StatisticsUpdate update)
{ {
@ -81,18 +49,12 @@ internal sealed class InfluxStatisticsClient : IDisposable
return; return;
DateTime date = DateTime.UtcNow; DateTime date = DateTime.UtcNow;
date = new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, DateTimeKind.Utc);
IReadOnlyDictionary<Character, Currencies> currencyStats = update.Currencies; IReadOnlyDictionary<Character, Currencies> currencyStats = update.Currencies;
var validFcIds = currencyStats.Keys var validFcIds = currencyStats.Keys
.Where(x => x.CharacterType == CharacterType.Character) .Where(x => x.CharacterType == CharacterType.Character)
.Where(x => _configuration.IncludedCharacters
.Any(config => config.LocalContentId == x.CharacterId && config.IncludeFreeCompany))
.Select(x => x.FreeCompanyId) .Select(x => x.FreeCompanyId)
.ToList(); .ToList();
var client = _influxClient;
if (client == null)
return;
Task.Run(async () => Task.Run(async () =>
{ {
try try
@ -102,16 +64,137 @@ internal sealed class InfluxStatisticsClient : IDisposable
{ {
if (character.CharacterType == CharacterType.Character) if (character.CharacterType == CharacterType.Character)
{ {
values.AddRange(GenerateCharacterStats(character, currencies, update, date)); update.LocalStats.TryGetValue(character, out LocalStats? localStats);
values.Add(PointData.Measurement("currency")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", character.Name)
.Tag("type", character.CharacterType.ToString())
.Tag("fc_id", character.FreeCompanyId > 0 ? character.FreeCompanyId.ToString() : null)
.Field("gil", localStats?.Gil ?? currencies.Gil)
.Field("ventures", currencies.Ventures)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits)
.Timestamp(date, WritePrecision.S));
if (localStats != null)
{
values.Add(PointData.Measurement("grandcompany")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", character.Name)
.Tag("type", character.CharacterType.ToString())
.Tag("fc_id", character.FreeCompanyId > 0 ? character.FreeCompanyId.ToString() : null)
.Field("gc", localStats.GrandCompany)
.Field("gc_rank", localStats.GcRank)
.Field("seals", (GrandCompany)localStats.GrandCompany switch
{
GrandCompany.Maelstrom => currencies.GcSealsMaelstrom,
GrandCompany.TwinAdder => currencies.GcSealsTwinAdders,
GrandCompany.ImmortalFlames => currencies.GcSealsImmortalFlames,
_ => 0,
})
.Field("seal_cap", localStats.GcRank switch
{
1 => 10_000,
2 => 15_000,
3 => 20_000,
4 => 25_000,
5 => 30_000,
6 => 35_000,
7 => 40_000,
8 => 45_000,
9 => 50_000,
10 => 80_000,
11 => 90_000,
_ => 0,
})
.Field("squadron_unlocked", localStats.SquadronUnlocked ? 1 : 0)
.Timestamp(date, WritePrecision.S));
if (localStats.ClassJobLevels.Count > 0)
{
foreach (var (expIndex, abbreviation) in _expToJobs)
{
var level = localStats.ClassJobLevels[expIndex];
if (level > 0)
{
values.Add(PointData.Measurement("experience")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", character.Name)
.Tag("type", character.CharacterType.ToString())
.Tag("fc_id", character.FreeCompanyId > 0 ? character.FreeCompanyId.ToString() : null)
.Tag("job", abbreviation)
.Field("level", level)
.Timestamp(date, WritePrecision.S));
}
}
}
if (localStats.MsqCount != -1)
{
values.Add(PointData.Measurement("quests")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", character.Name)
.Tag("msq_name", localStats.MsqName)
.Tag("fc_id",
character.FreeCompanyId > 0 ? character.FreeCompanyId.ToString() : null)
.Field("msq_count", localStats.MsqCount)
.Field("msq_genre", localStats.MsqGenre)
.Timestamp(date, WritePrecision.S));
}
}
} }
else if (character.CharacterType == CharacterType.Retainer) else if (character.CharacterType == CharacterType.Retainer)
{ {
values.AddRange(GenerateRetainerStats(character, currencies, update, date)); var owner = currencyStats.Keys.First(x => x.CharacterId == character.OwnerId);
values.Add(PointData.Measurement("currency")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", owner.Name)
.Tag("player_id", character.OwnerId.ToString())
.Tag("type", character.CharacterType.ToString())
.Tag("retainer_name", character.Name)
.Field("gil", currencies.Gil)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits)
.Timestamp(date, WritePrecision.S));
if (update.LocalStats.TryGetValue(owner, out var ownerStats) && character.ClassJob != 0)
{
values.Add(PointData.Measurement("retainer")
.Tag("id", character.CharacterId.ToString())
.Tag("player_name", owner.Name)
.Tag("player_id", character.OwnerId.ToString())
.Tag("type", character.CharacterType.ToString())
.Tag("retainer_name", character.Name)
.Tag("class", _classJobNames[character.ClassJob])
.Field("level", character.Level)
.Field("is_max_level", character.Level == ownerStats.MaxLevel ? 1 : 0)
.Field("can_reach_max_level",
ownerStats.ClassJobLevels.Count > 0 &&
ownerStats.ClassJobLevels[_classJobToArrayIndex[character.ClassJob]] ==
ownerStats.MaxLevel
? 1
: 0)
.Field("levels_before_cap",
ownerStats.ClassJobLevels.Count > 0
? ownerStats.ClassJobLevels[_classJobToArrayIndex[character.ClassJob]] -
character.Level
: 0)
.Timestamp(date, WritePrecision.S));
}
} }
else if (character.CharacterType == CharacterType.FreeCompanyChest && else if (character.CharacterType == CharacterType.FreeCompanyChest &&
validFcIds.Contains(character.CharacterId)) validFcIds.Contains(character.CharacterId))
{ {
values.AddRange(GenerateFcStats(character, currencies, update, date)); values.Add(PointData.Measurement("currency")
.Tag("id", character.CharacterId.ToString())
.Tag("fc_name", character.Name)
.Tag("type", character.CharacterType.ToString())
.Field("gil", currencies.Gil)
.Field("fccredit", currencies.FcCredits)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits)
.Timestamp(date, WritePrecision.S));
} }
} }
@ -122,8 +205,7 @@ internal sealed class InfluxStatisticsClient : IDisposable
foreach (var sub in subs) foreach (var sub in subs)
{ {
values.Add(PointData.Measurement("submersibles") values.Add(PointData.Measurement("submersibles")
.Tag("id", fc.CharacterId.ToString(CultureInfo.InvariantCulture)) .Tag("id", fc.CharacterId.ToString())
.Tag("world", _worldNames[fc.WorldId])
.Tag("fc_name", fc.Name) .Tag("fc_name", fc.Name)
.Tag("sub_id", $"{fc.CharacterId}_{sub.Id}") .Tag("sub_id", $"{fc.CharacterId}_{sub.Id}")
.Tag("sub_name", sub.Name) .Tag("sub_name", sub.Name)
@ -132,221 +214,28 @@ internal sealed class InfluxStatisticsClient : IDisposable
.Tag("part_bow", sub.Bow) .Tag("part_bow", sub.Bow)
.Tag("part_bridge", sub.Bridge) .Tag("part_bridge", sub.Bridge)
.Tag("build", sub.Build) .Tag("build", sub.Build)
.Field("enabled", sub.Enabled ? 1 : 0)
.Field("level", sub.Level) .Field("level", sub.Level)
.Field("predicted_level", sub.PredictedLevel)
.Field("state", (int)sub.State)
.Field("return_time", new DateTimeOffset(sub.ReturnTime).ToUnixTimeSeconds())
.Timestamp(date, WritePrecision.S)); .Timestamp(date, WritePrecision.S));
} }
} }
} }
var writeApi = client.GetWriteApiAsync(); var writeApi = _influxClient.GetWriteApiAsync();
await writeApi.WritePointsAsync( await writeApi.WritePointsAsync(
values, values,
_configuration.Server.Bucket, _configuration.Server.Organization) _configuration.Server.Bucket, _configuration.Server.Organization);
.ConfigureAwait(false);
_pluginLog.Verbose($"Influx: Sent {values.Count} data points to server"); //_chatGui.Print($"Influx: {values.Count} points");
} }
catch (Exception e) catch (Exception e)
{ {
_pluginLog.Error(e, "Unable to update statistics");
_chatGui.PrintError(e.Message); _chatGui.PrintError(e.Message);
} }
}); });
} }
private IEnumerable<PointData> GenerateCharacterStats(Character character, Currencies currencies,
StatisticsUpdate update, DateTime date)
{
update.LocalStats.TryGetValue(character, out LocalStats? localStats);
bool includeFc = character.FreeCompanyId > 0 &&
_configuration.IncludedCharacters.Any(x =>
x.LocalContentId == character.CharacterId && x.IncludeFreeCompany);
Func<string, PointData> pointData = s => PointData.Measurement(s)
.Tag("id", character.CharacterId.ToString(CultureInfo.InvariantCulture))
.Tag("player_name", character.Name)
.Tag("world", _worldNames[character.WorldId])
.Tag("type", character.CharacterType.ToString())
.Tag("fc_id", includeFc ? character.FreeCompanyId.ToString(CultureInfo.InvariantCulture) : null)
.Timestamp(date, WritePrecision.S);
yield return pointData("currency")
.Field("gil", localStats?.Gil ?? 0)
.Field("mgp", localStats?.MGP ?? 0)
.Field("ventures", currencies.Ventures)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits)
.Field("free_inventory", currencies.FreeSlots);
if (localStats != null)
{
yield return pointData("grandcompany")
.Field("gc", localStats.GrandCompany)
.Field("gc_rank", localStats.GcRank)
.Field("seals", (GrandCompany)localStats.GrandCompany switch
{
GrandCompany.Maelstrom => currencies.GcSealsMaelstrom,
GrandCompany.TwinAdder => currencies.GcSealsTwinAdders,
GrandCompany.ImmortalFlames => currencies.GcSealsImmortalFlames,
_ => 0,
})
.Field("seal_cap", localStats.GcRank switch
{
1 => 10_000,
2 => 15_000,
3 => 20_000,
4 => 25_000,
5 => 30_000,
6 => 35_000,
7 => 40_000,
8 => 45_000,
9 => 50_000,
10 => 80_000,
11 => 90_000,
_ => 0,
})
.Field("squadron_unlocked", localStats.SquadronUnlocked ? 1 : 0);
if (localStats.ClassJobLevels.Count > 0)
{
foreach (var (expIndex, job) in _expToJobs)
{
// last update to this char was in 6.x, so we don't have PCT/VPR data
if (localStats.ClassJobLevels.Count <= expIndex)
continue;
var level = localStats.ClassJobLevels[expIndex];
if (level > 0)
{
yield return pointData("experience")
.Tag("job", job.Abbreviation)
.Tag("job_type", job.Type)
.Field("level", level);
}
}
}
if (localStats.MsqCount != -1)
{
yield return pointData("quests")
.Tag("msq_name", localStats.MsqName)
.Field("msq_count", localStats.MsqCount)
.Field("msq_genre", localStats.MsqGenre);
}
}
foreach (var inventoryPoint in GenerateInventoryStats(character.CharacterId, update, pointData))
yield return inventoryPoint;
}
private IEnumerable<PointData> GenerateRetainerStats(Character character, Currencies currencies,
StatisticsUpdate update, DateTime date)
{
var owner = update.Currencies.Keys.First(x => x.CharacterId == character.OwnerId);
Func<string, PointData> pointData = s => PointData.Measurement(s)
.Tag("id", character.CharacterId.ToString(CultureInfo.InvariantCulture))
.Tag("player_name", owner.Name)
.Tag("player_id", character.OwnerId.ToString(CultureInfo.InvariantCulture))
.Tag("world", _worldNames[character.WorldId])
.Tag("type", character.CharacterType.ToString())
.Tag("retainer_name", character.Name)
.Timestamp(date, WritePrecision.S);
yield return pointData("currency")
.Field("gil", currencies.Gil)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits);
if (update.LocalStats.TryGetValue(owner, out var ownerStats) && character.ClassJob != 0)
{
yield return pointData("retainer")
.Tag("class", _classJobNames[character.ClassJob])
.Field("level", character.Level)
.Field("is_max_level", character.Level == ownerStats.MaxLevel ? 1 : 0)
.Field("can_reach_max_level",
ownerStats.ClassJobLevels.Count > 0 &&
ownerStats.ClassJobLevels[_classJobToArrayIndex[character.ClassJob]] ==
ownerStats.MaxLevel
? 1
: 0)
.Field("levels_before_cap",
ownerStats.ClassJobLevels.Count > 0
? ownerStats.ClassJobLevels[_classJobToArrayIndex[character.ClassJob]] -
character.Level
: 0);
}
foreach (var inventoryPoint in GenerateInventoryStats(character.CharacterId, update, pointData))
yield return inventoryPoint;
}
private IEnumerable<PointData> GenerateInventoryStats(ulong localContentId, StatisticsUpdate update,
Func<string, PointData> pointData)
{
foreach (var (filterName, items) in update.InventoryItems)
{
foreach (var item in items.Where(x => x.LocalContentId == localContentId)
.GroupBy(x => new { x.ItemId, x.IsHq }))
{
_prices.TryGetValue(item.Key.ItemId, out PriceInfo priceInfo);
bool priceHq = item.Key.IsHq || priceInfo.UiCategory == 58; // materia always uses HQ prices
yield return pointData("items")
.Tag("filter_name", filterName)
.Tag("item_id", item.Key.ItemId.ToString(CultureInfo.InvariantCulture))
.Tag("item_name", priceInfo.Name)
.Tag("hq", (item.Key.IsHq ? 1 : 0).ToString(CultureInfo.InvariantCulture))
.Field("quantity", item.Sum(x => x.Quantity))
.Field("total_gil", item.Sum(x => x.Quantity) * (priceHq ? priceInfo.Hq : priceInfo.Normal));
}
}
}
private IEnumerable<PointData> GenerateFcStats(Character character, Currencies currencies, StatisticsUpdate update,
DateTime date)
{
update.FcStats.TryGetValue(character.CharacterId, out FcStats? fcStats);
Func<string, PointData> pointData = s => PointData.Measurement(s)
.Tag("id", character.CharacterId.ToString(CultureInfo.InvariantCulture))
.Tag("fc_name", character.Name)
.Tag("world", _worldNames[character.WorldId])
.Tag("type", character.CharacterType.ToString())
.Timestamp(date, WritePrecision.S);
yield return pointData("currency")
.Field("gil", currencies.Gil)
.Field("fccredit", fcStats?.FcCredits ?? 0)
.Field("ceruleum_tanks", currencies.CeruleumTanks)
.Field("repair_kits", currencies.RepairKits);
foreach (var inventoryPoint in GenerateInventoryStats(character.CharacterId, update, pointData))
yield return inventoryPoint;
}
public void Dispose() public void Dispose()
{ {
_influxClient?.Dispose(); _influxClient.Dispose();
}
private struct PriceInfo
{
public string Name { get; init; }
public uint Normal { get; init; }
public uint Hq => Normal + (uint)Math.Ceiling((decimal)Normal / 10);
public uint UiCategory { get; set; }
}
private sealed record ClassJobDetail(string Abbreviation, bool IsNonCombat)
{
public string Type => IsNonCombat ? "doh_dol" : "combat";
} }
} }

View File

@ -1,80 +1,84 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Timers; using System.Threading;
using Dalamud.Game.ClientState.Conditions; using AutoRetainerAPI;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Automation;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Influx.AllaganTools; using Influx.AllaganTools;
using Influx.Influx; using Influx.Influx;
using Influx.LocalStatistics; using Influx.LocalStatistics;
using Influx.SubmarineTracker; using Influx.SubmarineTracker;
using Influx.Windows; using Influx.Windows;
using LLib; using LLib;
using Task = System.Threading.Tasks.Task;
namespace Influx; namespace Influx;
[SuppressMessage("ReSharper", "UnusedType.Global")] [SuppressMessage("ReSharper", "UnusedType.Global")]
[SuppressMessage("Performance", "CA1812")] public class InfluxPlugin : IDalamudPlugin
internal sealed class InfluxPlugin : IDalamudPlugin
{ {
private readonly object _lock = new(); private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly ICondition _condition;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly AllaganToolsIpc _allaganToolsIpc; private readonly AllaganToolsIpc _allaganToolsIpc;
private readonly SubmarineTrackerIpc _submarineTrackerIpc; private readonly SubmarineTrackerIpc _submarineTrackerIpc;
private readonly LocalStatsCalculator _localStatsCalculator; private readonly LocalStatsCalculator _localStatsCalculator;
private readonly FcStatsCalculator _fcStatsCalculator;
private readonly InfluxStatisticsClient _influxStatisticsClient; private readonly InfluxStatisticsClient _influxStatisticsClient;
private readonly WindowSystem _windowSystem; private readonly WindowSystem _windowSystem;
private readonly StatisticsWindow _statisticsWindow; private readonly StatisticsWindow _statisticsWindow;
private readonly ConfigurationWindow _configurationWindow; private readonly ConfigurationWindow _configurationWindow;
private readonly Timer _timer; private readonly Timer _timer;
private readonly AutoRetainerApi _autoRetainerApi;
public InfluxPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog, private bool closeFcWindow = false;
public InfluxPlugin(DalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog,
ICommandManager commandManager, IChatGui chatGui, IDataManager dataManager, IFramework framework, ICommandManager commandManager, IChatGui chatGui, IDataManager dataManager, IFramework framework,
IAddonLifecycle addonLifecycle, IGameGui gameGui, ICondition condition) IAddonLifecycle addonLifecycle, IGameGui gameGui)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = LoadConfig(); _configuration = LoadConfig();
_clientState = clientState; _clientState = clientState;
_commandManager = commandManager; _commandManager = commandManager;
_condition = condition;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
DalamudReflector dalamudReflector = new DalamudReflector(pluginInterface, framework, pluginLog); DalamudReflector dalamudReflector = new DalamudReflector(pluginInterface, framework, pluginLog);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, chatGui, dalamudReflector, framework, _pluginLog); _allaganToolsIpc = new AllaganToolsIpc(pluginInterface, chatGui, dalamudReflector, framework, _pluginLog);
_submarineTrackerIpc = new SubmarineTrackerIpc(dalamudReflector); _submarineTrackerIpc = new SubmarineTrackerIpc(dalamudReflector);
_localStatsCalculator = _localStatsCalculator = new LocalStatsCalculator(pluginInterface, clientState, addonLifecycle, pluginLog, dataManager);
new LocalStatsCalculator(pluginInterface, clientState, addonLifecycle, pluginLog, dataManager); _influxStatisticsClient = new InfluxStatisticsClient(chatGui, _configuration, dataManager, clientState);
_fcStatsCalculator = new FcStatsCalculator(this, pluginInterface, clientState, addonLifecycle, gameGui,
framework, _configuration, pluginLog);
_influxStatisticsClient =
new InfluxStatisticsClient(chatGui, _configuration, dataManager, clientState, _pluginLog);
_windowSystem = new WindowSystem(typeof(InfluxPlugin).FullName); _windowSystem = new WindowSystem(typeof(InfluxPlugin).FullName);
_statisticsWindow = new StatisticsWindow(); _statisticsWindow = new StatisticsWindow();
_windowSystem.AddWindow(_statisticsWindow); _windowSystem.AddWindow(_statisticsWindow);
_configurationWindow = new ConfigurationWindow(_pluginInterface, clientState, _configuration, _allaganToolsIpc); _configurationWindow = new ConfigurationWindow(_pluginInterface, clientState, _configuration);
_configurationWindow.ConfigUpdated += (_, _) => _influxStatisticsClient.UpdateClient();
_windowSystem.AddWindow(_configurationWindow); _windowSystem.AddWindow(_configurationWindow);
_commandManager.AddHandler("/influx", new CommandInfo(ProcessCommand)); _commandManager.AddHandler("/influx", new CommandInfo(ProcessCommand));
_timer = new Timer(_ => UpdateStatistics(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
_timer = new Timer(TimeSpan.FromMinutes(1));
_timer.Elapsed += (_, _) => UpdateStatistics();
_timer.AutoReset = true;
_timer.Enabled = true;
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configurationWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi += _configurationWindow.Toggle;
_condition.ConditionChange += UpdateOnLogout;
ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new();
_autoRetainerApi.OnCharacterPostprocessStep += CheckCharacterPostProcess;
_autoRetainerApi.OnCharacterReadyToPostProcess += DoCharacterPostProcess;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup ,"FreeCompany", CloseFcWindow);
} }
private Configuration LoadConfig() private Configuration LoadConfig()
@ -89,139 +93,118 @@ internal sealed class InfluxPlugin : IDalamudPlugin
private void ProcessCommand(string command, string arguments) private void ProcessCommand(string command, string arguments)
{ {
if (arguments == "gil") if (arguments == "c" || arguments == "config")
_configurationWindow.Toggle();
else
{ {
UpdateStatistics(); UpdateStatistics();
_statisticsWindow.IsOpen = true; _statisticsWindow.IsOpen = true;
} }
else
_configurationWindow.Toggle();
} }
private void UpdateStatistics() private void UpdateStatistics()
{ {
lock (_lock) if (!_clientState.IsLoggedIn ||
_configuration.IncludedCharacters.All(x => x.LocalContentId != _clientState.LocalContentId))
return;
try
{ {
if (!_clientState.IsLoggedIn || var currencies = _allaganToolsIpc.CountCurrencies();
_configuration.IncludedCharacters.All(x => x.LocalContentId != _clientState.LocalContentId)) var characters = currencies.Keys.ToList();
{ if (characters.Count == 0)
_pluginLog.Verbose("Influx: not logged in or not enabled for this character");
return; return;
}
try var update = new StatisticsUpdate
{ {
var currencies = _allaganToolsIpc.CountCurrencies(); Currencies = currencies
var characters = currencies.Keys.ToList(); .Where(x => _configuration.IncludedCharacters.Any(y =>
if (characters.Count == 0) y.LocalContentId == x.Key.CharacterId ||
{ y.LocalContentId == x.Key.OwnerId ||
_pluginLog.Warning("Found 0 AllaganTools characters"); characters.Any(z => y.LocalContentId == z.CharacterId && z.FreeCompanyId == x.Key.CharacterId)))
return; .ToDictionary(x => x.Key, x => x.Value),
} Submarines = _submarineTrackerIpc.GetSubmarineStats(characters),
LocalStats = _localStatsCalculator.GetAllCharacterStats()
foreach (Character character in characters) .Where(x => characters.Any(y => y.CharacterId == x.Key))
{ .ToDictionary(x => characters.First(y => y.CharacterId == x.Key), x => x.Value)
if (character.CharacterType == CharacterType.Character && character.FreeCompanyId != default) .Where(x => _configuration.IncludedCharacters.Any(y =>
{ y.LocalContentId == x.Key.CharacterId ||
bool isFcEnabled = _configuration.IncludedCharacters y.LocalContentId == x.Key.OwnerId ||
.FirstOrDefault(x => x.LocalContentId == character.CharacterId)?.IncludeFreeCompany ?? true; characters.Any(z => y.LocalContentId == z.CharacterId && z.FreeCompanyId == x.Key.CharacterId)))
if (!isFcEnabled) .ToDictionary(x => x.Key, x => x.Value),
character.FreeCompanyId = default; };
} _statisticsWindow.OnStatisticsUpdate(update);
} _influxStatisticsClient.OnStatisticsUpdate(update);
}
Dictionary<string, IReadOnlyList<SortingResult>> inventoryItems = catch (Exception e)
_configuration.IncludedInventoryFilters.Select(c => c.Name) {
.Distinct() _pluginLog.Error(e, "failed to update statistics");
.ToDictionary(c => c, c =>
{
var filter = _allaganToolsIpc.GetFilter(c);
if (filter == null)
return new List<SortingResult>();
return filter.GenerateFilteredList();
});
var update = new StatisticsUpdate
{
Currencies = currencies
.Where(x => _configuration.IncludedCharacters.Any(y =>
y.LocalContentId == x.Key.CharacterId ||
y.LocalContentId == x.Key.OwnerId ||
characters.Any(z =>
y.LocalContentId == z.CharacterId && z.FreeCompanyId == x.Key.CharacterId)))
.ToDictionary(x => x.Key, x => x.Value),
InventoryItems = inventoryItems,
Submarines = UpdateEnabledSubs(_submarineTrackerIpc.GetSubmarineStats(characters), characters),
LocalStats = _localStatsCalculator.GetAllCharacterStats()
.Where(x => characters.Any(y => y.CharacterId == x.Key))
.ToDictionary(x => characters.First(y => y.CharacterId == x.Key), x => x.Value)
.Where(x => _configuration.IncludedCharacters.Any(y =>
y.LocalContentId == x.Key.CharacterId ||
y.LocalContentId == x.Key.OwnerId ||
characters.Any(z =>
y.LocalContentId == z.CharacterId && z.FreeCompanyId == x.Key.CharacterId)))
.ToDictionary(x => x.Key, x => x.Value),
FcStats = _fcStatsCalculator.GetAllFcStats()
.Where(x => characters.Any(y => y.FreeCompanyId == x.Key))
.ToDictionary(x => x.Key, x => x.Value),
};
_statisticsWindow.OnStatisticsUpdate(update);
_influxStatisticsClient.OnStatisticsUpdate(update);
}
catch (Exception e)
{
_pluginLog.Error(e, "failed to update statistics");
}
} }
} }
private IReadOnlyDictionary<Character, List<SubmarineStats>> UpdateEnabledSubs( private unsafe void CheckCharacterPostProcess()
IReadOnlyDictionary<Character, List<SubmarineStats>> allSubs, List<Character> characters)
{ {
foreach (var (character, subs) in allSubs)
var infoProxy = Framework.Instance()->UIModule->GetInfoModule()->GetInfoProxyById(InfoProxyId.FreeCompany);
if (infoProxy != null)
{ {
var owner = characters.FirstOrDefault(x => x.FreeCompanyId == character.CharacterId); var fcProxy = (InfoProxyFreeCompany*)infoProxy;
if (owner == null) if (fcProxy->ID != 0)
continue; {
_pluginLog.Information($"Requesting post-process, FC is {fcProxy->ID}");
var enabledSubs = _fcStatsCalculator.GetEnabledSubs(owner.CharacterId); _autoRetainerApi.RequestCharacterPostprocess();
foreach (var sub in subs) }
sub.Enabled = enabledSubs.Contains(sub.Name); else
_pluginLog.Information("No FC id");
} }
else
_pluginLog.Information("No FreeCompany info proxy");
return allSubs;
} }
private void UpdateOnLogout(ConditionFlag flag, bool value) private void DoCharacterPostProcess()
{ {
if (flag == ConditionFlag.LoggingOut && value) closeFcWindow = true;
Chat.Instance.SendMessage("/freecompanycmd");
}
private void CloseFcWindow(AddonEvent type, AddonArgs args)
{
if (closeFcWindow)
{ {
try Task.Run(async () =>
{ {
_timer.Enabled = false; // this runs every 500ms
_localStatsCalculator.UpdateStatisticsLogout(); // https://github.com/Critical-Impact/CriticalCommonLib/blob/7b3814e703dd5b2981cd4334524b4b301c23e639/Services/InventoryScanner.cs#L436
UpdateStatistics(); await Task.Delay(550);
}
finally _pluginLog.Information("Closing FC window...");
{ unsafe
_timer.Enabled = true; {
} AtkUnitBase* addon = (AtkUnitBase*)_gameGui.GetAddonByName("FreeCompany");
if (addon->IsVisible)
addon->FireCallbackInt(-1);
}
closeFcWindow = false;
_autoRetainerApi.FinishCharacterPostProcess();
});
} }
} }
public void Dispose() public void Dispose()
{ {
_condition.ConditionChange -= UpdateOnLogout; _addonLifecycle.UnregisterListener(AddonEvent.PostSetup ,"FreeCompany", CloseFcWindow);
_autoRetainerApi.OnCharacterPostprocessStep -= CheckCharacterPostProcess;
_autoRetainerApi.OnCharacterReadyToPostProcess -= DoCharacterPostProcess;
_autoRetainerApi.Dispose();
ECommonsMain.Dispose();
_pluginInterface.UiBuilder.OpenConfigUi -= _configurationWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi -= _configurationWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_timer.Stop();
_timer.Dispose(); _timer.Dispose();
_windowSystem.RemoveAllWindows(); _windowSystem.RemoveAllWindows();
_commandManager.RemoveHandler("/influx"); _commandManager.RemoveHandler("/influx");
_influxStatisticsClient.Dispose(); _influxStatisticsClient.Dispose();
_fcStatsCalculator.Dispose();
_localStatsCalculator.Dispose(); _localStatsCalculator.Dispose();
_allaganToolsIpc.Dispose(); _allaganToolsIpc.Dispose();
} }

View File

@ -1,7 +0,0 @@
namespace Influx.LocalStatistics;
public sealed record FcStats
{
public ulong ContentId { get; init; }
public int FcCredits { get; init; }
}

View File

@ -1,243 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutoRetainerAPI;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Automation;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Newtonsoft.Json;
namespace Influx.LocalStatistics;
internal sealed class FcStatsCalculator : IDisposable
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly Configuration _configuration;
private readonly IPluginLog _pluginLog;
private readonly AutoRetainerApi _autoRetainerApi;
private readonly Dictionary<ulong, FcStats> _cache = new();
private Status? _status;
public FcStatsCalculator(
IDalamudPlugin plugin,
IDalamudPluginInterface pluginInterface,
IClientState clientState,
IAddonLifecycle addonLifecycle,
IGameGui gameGui,
IFramework framework,
Configuration configuration,
IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_clientState = clientState;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_framework = framework;
_configuration = configuration;
_pluginLog = pluginLog;
ECommonsMain.Init(_pluginInterface, plugin);
_autoRetainerApi = new();
_autoRetainerApi.OnCharacterPostprocessStep += CheckCharacterPostProcess;
_autoRetainerApi.OnCharacterReadyToPostProcess += DoCharacterPostProcess;
_addonLifecycle.RegisterListener(AddonEvent.PostReceiveEvent, "FreeCompany", FcPostReceiveEvent);
_framework.Update += FrameworkUpdate;
_clientState.Logout += Logout;
foreach (var file in _pluginInterface.ConfigDirectory.GetFiles("f.*.json"))
{
try
{
var stats = JsonConvert.DeserializeObject<FcStats>(File.ReadAllText(file.FullName));
if (stats == null)
continue;
_cache[stats.ContentId] = stats;
}
catch (Exception e)
{
_pluginLog.Warning(e, $"Could not parse file {file.FullName}");
}
}
}
private unsafe void CheckCharacterPostProcess()
{
bool includeFc = _configuration.IncludedCharacters.Any(x =>
x.LocalContentId == _clientState.LocalContentId &&
x.IncludeFreeCompany);
if (!includeFc)
return;
var infoProxy = Framework.Instance()->UIModule->GetInfoModule()->GetInfoProxyById(InfoProxyId.FreeCompany);
if (infoProxy != null)
{
var fcProxy = (InfoProxyFreeCompany*)infoProxy;
if (fcProxy->Id != 0)
{
_pluginLog.Information($"Requesting post-process, FC is {fcProxy->Id}");
_autoRetainerApi.RequestCharacterPostprocess();
}
else
_pluginLog.Information("No FC id");
}
else
_pluginLog.Information("No FreeCompany info proxy");
}
private void DoCharacterPostProcess()
{
_status = new();
unsafe
{
AtkUnitBase* addon = (AtkUnitBase*)_gameGui.GetAddonByName("FreeCompany");
if (addon != null && addon->IsVisible)
FcPostReceiveEvent(AddonEvent.PostReceiveEvent);
else
Chat.Instance.SendMessage("/freecompanycmd");
}
}
private void FcPostReceiveEvent(AddonEvent type, AddonArgs? args = null)
{
if (_status != null)
{
_pluginLog.Verbose("FC window received event...");
_status.WindowOpened = true;
}
else
_pluginLog.Verbose("Not tracking status for FC window");
}
private void FrameworkUpdate(IFramework framework)
{
if (_status == null)
return;
if (_status.FallbackFinishPostProcessing < DateTime.Now)
{
_status = null;
_autoRetainerApi.FinishCharacterPostProcess();
}
else if (_status.WindowOpened && UpdateFcCredits())
{
_status = null;
_autoRetainerApi.FinishCharacterPostProcess();
}
}
private void Logout()
{
if (_status != null)
_autoRetainerApi.FinishCharacterPostProcess();
_status = null;
}
// ideally we'd hook the update to the number array, but #effort
private unsafe bool UpdateFcCredits()
{
try
{
var infoProxy =
Framework.Instance()->UIModule->GetInfoModule()->GetInfoProxyById(InfoProxyId.FreeCompany);
if (infoProxy != null)
{
var fcProxy = (InfoProxyFreeCompany*)infoProxy;
ulong localContentId = fcProxy->Id;
if (localContentId != 0)
{
var atkArrays = Framework.Instance()->GetUIModule()->GetRaptureAtkModule()->AtkModule
.AtkArrayDataHolder;
if (atkArrays.NumberArrayCount > 51)
{
var fcArrayData = atkArrays.GetNumberArrayData(51);
FcStats fcStats = new FcStats
{
ContentId = localContentId,
FcCredits = fcArrayData->IntArray[9]
};
_pluginLog.Verbose($"Current FC credits: {fcStats.FcCredits:N0}");
if (fcStats.FcCredits > 0)
{
if (_cache.TryGetValue(localContentId, out var existingStats))
{
if (existingStats != fcStats)
{
_cache[localContentId] = fcStats;
File.WriteAllText(
Path.Join(_pluginInterface.GetPluginConfigDirectory(),
$"f.{localContentId:X8}.json"),
JsonConvert.SerializeObject(fcStats));
}
}
else
{
_cache[localContentId] = fcStats;
File.WriteAllText(
Path.Join(_pluginInterface.GetPluginConfigDirectory(),
$"f.{localContentId:X8}.json"),
JsonConvert.SerializeObject(fcStats));
}
return true;
}
}
return false;
}
else
// no point updating if no fc id
return true;
}
}
catch (Exception e)
{
_pluginLog.Warning(e, "Unable to update fc credits");
}
return false;
}
public IReadOnlyDictionary<ulong, FcStats> GetAllFcStats() => _cache.AsReadOnly();
public HashSet<string> GetEnabledSubs(ulong characterId)
{
var offlineCharacterData = _autoRetainerApi.GetOfflineCharacterData(characterId);
if (offlineCharacterData == null || !offlineCharacterData.WorkshopEnabled)
return [];
return offlineCharacterData.EnabledSubs;
}
public void Dispose()
{
_clientState.Logout -= Logout;
_framework.Update -= FrameworkUpdate;
_addonLifecycle.UnregisterListener(AddonEvent.PostReceiveEvent, "FreeCompany", FcPostReceiveEvent);
_autoRetainerApi.OnCharacterPostprocessStep -= CheckCharacterPostProcess;
_autoRetainerApi.OnCharacterReadyToPostProcess -= DoCharacterPostProcess;
_autoRetainerApi.Dispose();
ECommonsMain.Dispose();
}
private sealed class Status
{
public bool WindowOpened { get; set; }
public DateTime FallbackFinishPostProcessing { get; set; } = DateTime.Now.AddSeconds(10);
}
}

View File

@ -2,18 +2,17 @@
namespace Influx.LocalStatistics; namespace Influx.LocalStatistics;
public sealed record LocalStats public record LocalStats
{ {
public ulong ContentId { get; init; } public ulong ContentId { get; init; }
public byte GrandCompany { get; init; } public byte GrandCompany { get; init; }
public byte GcRank { get; init; } public byte GcRank { get; init; }
public bool SquadronUnlocked { get; init; } public bool SquadronUnlocked { get; init; }
public byte MaxLevel { get; init; } = 90; public byte MaxLevel { get; init; } = 90;
public IList<short> ClassJobLevels { get; init; } = new List<short>(); public List<short> ClassJobLevels { get; set; } = new();
public byte StartingTown { get; init; } public byte StartingTown { get; init; }
public int MsqCount { get; set; } = -1; public int MsqCount { get; set; } = -1;
public string? MsqName { get; set; } public string? MsqName { get; set; }
public uint MsqGenre { get; set; } public uint MsqGenre { get; set; }
public int Gil { get; set; } public int? Gil { get; set; }
public int MGP { get; set; }
} }

View File

@ -1,15 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -27,10 +28,11 @@ internal sealed class LocalStatsCalculator : IDisposable
private const uint JointQuest = 65781; private const uint JointQuest = 65781;
private readonly IDalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly GameStrings _gameStrings;
private readonly Dictionary<ulong, LocalStats> _cache = new(); private readonly Dictionary<ulong, LocalStats> _cache = new();
private IReadOnlyList<QuestInfo>? _gridaniaStart; private IReadOnlyList<QuestInfo>? _gridaniaStart;
@ -40,7 +42,7 @@ internal sealed class LocalStatsCalculator : IDisposable
public LocalStatsCalculator( public LocalStatsCalculator(
IDalamudPluginInterface pluginInterface, DalamudPluginInterface pluginInterface,
IClientState clientState, IClientState clientState,
IAddonLifecycle addonLifecycle, IAddonLifecycle addonLifecycle,
IPluginLog pluginLog, IPluginLog pluginLog,
@ -50,17 +52,21 @@ internal sealed class LocalStatsCalculator : IDisposable
_clientState = clientState; _clientState = clientState;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_gameStrings = new GameStrings(dataManager, pluginLog);
_clientState.Login += UpdateStatistics; _clientState.Login += UpdateStatistics;
_clientState.TerritoryChanged += UpdateStatistics; _clientState.TerritoryChanged += UpdateStatistics;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", UpdateStatisticsLogout);
_addonLifecycle.RegisterListener(AddonEvent.PreSetup, "JournalAccept", UpdateStatistics); _addonLifecycle.RegisterListener(AddonEvent.PreSetup, "JournalAccept", UpdateStatistics);
Task.Run(() => Task.Run(() =>
{ {
List<QuestInfo> msq = new(); List<QuestInfo> msq = new();
foreach (var quest in dataManager.GetExcelSheet<Quest>()!.Where(x => x.JournalGenre.Row is >= 1 and <= 13)) foreach (var quest in dataManager.GetExcelSheet<Quest>()!.Where(x => x.JournalGenre.Row is >= 1 and <= 12))
{ {
var previousQuests = quest.PreviousQuest?.Select(x => x.Row).Where(x => x != 0).ToList(); var previousQuests = quest.PreviousQuest?.Select(x => x.Row).Where(x => x != 0).ToList();
if (previousQuests != null && quest.Unknown12 != 0)
previousQuests.Add(quest.Unknown12);
msq.Add(new QuestInfo msq.Add(new QuestInfo
{ {
@ -110,7 +116,7 @@ internal sealed class LocalStatsCalculator : IDisposable
UpdateStatistics(); UpdateStatistics();
} }
private static ReadOnlyCollection<QuestInfo> PopulateStartingCities(List<QuestInfo> quests, uint envoyQuestId, private IReadOnlyList<QuestInfo> PopulateStartingCities(List<QuestInfo> quests, uint envoyQuestId,
uint startingQuestId, bool popCallOfTheSea) uint startingQuestId, bool popCallOfTheSea)
{ {
QuestInfo callOfTheSea = quests.First(x => x.PreviousQuestIds.Contains(envoyQuestId)); QuestInfo callOfTheSea = quests.First(x => x.PreviousQuestIds.Contains(envoyQuestId));
@ -118,10 +124,12 @@ internal sealed class LocalStatsCalculator : IDisposable
quests.Remove(callOfTheSea); quests.Remove(callOfTheSea);
List<QuestInfo> startingCityQuests = new List<QuestInfo> { callOfTheSea }; List<QuestInfo> startingCityQuests = new List<QuestInfo> { callOfTheSea };
uint questId = envoyQuestId; uint? questId = envoyQuestId;
QuestInfo? quest;
do do
{ {
QuestInfo quest = quests.First(x => x.RowId == questId); quest = quests.First(x => x.RowId == questId);
quests.Remove(quest); quests.Remove(quest);
if (quest.Name == "Close to Home") if (quest.Name == "Close to Home")
@ -130,14 +138,14 @@ internal sealed class LocalStatsCalculator : IDisposable
{ {
RowId = startingQuestId, RowId = startingQuestId,
Name = "Coming to ...", Name = "Coming to ...",
PreviousQuestIds = new List<uint>(), PreviousQuestIds = new(),
Genre = quest.Genre, Genre = quest.Genre,
}; };
} }
startingCityQuests.Add(quest); startingCityQuests.Add(quest);
questId = quest.PreviousQuestIds.FirstOrDefault(); questId = quest.PreviousQuestIds.FirstOrDefault();
} while (questId != 0); } while (questId != null && questId != 0);
return Enumerable.Reverse(startingCityQuests).ToList().AsReadOnly(); return Enumerable.Reverse(startingCityQuests).ToList().AsReadOnly();
} }
@ -145,12 +153,20 @@ internal sealed class LocalStatsCalculator : IDisposable
public void Dispose() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PreSetup, "JournalAccept", UpdateStatistics); _addonLifecycle.UnregisterListener(AddonEvent.PreSetup, "JournalAccept", UpdateStatistics);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", UpdateStatisticsLogout);
_clientState.Login -= UpdateStatistics; _clientState.Login -= UpdateStatistics;
} }
private void UpdateStatistics(ushort territoryType) => UpdateStatistics(); private void UpdateStatistics(ushort territoryType) => UpdateStatistics();
public void UpdateStatisticsLogout() => UpdateStatistics(); private unsafe void UpdateStatisticsLogout(AddonEvent type, AddonArgs args)
{
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string? text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText)?.ToString();
text = text?.Replace("\n", "").Replace("\r", "");
if (text == _gameStrings.LogoutToTitleScreen || text == _gameStrings.LogoutAndExitGame)
UpdateStatistics();
}
private void UpdateStatistics(AddonEvent type, AddonArgs args) => UpdateStatistics(); private void UpdateStatistics(AddonEvent type, AddonArgs args) => UpdateStatistics();
@ -175,14 +191,11 @@ internal sealed class LocalStatsCalculator : IDisposable
ContentId = localContentId, ContentId = localContentId,
GrandCompany = playerState->GrandCompany, GrandCompany = playerState->GrandCompany,
GcRank = playerState->GetGrandCompanyRank(), GcRank = playerState->GetGrandCompanyRank(),
SquadronUnlocked = playerState->GetGrandCompanyRank() >= 9 && (QuestManager.IsQuestComplete(67925) || SquadronUnlocked = playerState->GetGrandCompanyRank() >= 9 && (QuestManager.IsQuestComplete(67925) || QuestManager.IsQuestComplete(67926) || QuestManager.IsQuestComplete(67927)),
QuestManager.IsQuestComplete(67926) ||
QuestManager.IsQuestComplete(67927)),
MaxLevel = playerState->MaxLevel, MaxLevel = playerState->MaxLevel,
ClassJobLevels = ExtractClassJobLevels(playerState), ClassJobLevels = ExtractClassJobLevels(playerState),
StartingTown = playerState->StartTown, StartingTown = playerState->StartTown,
Gil = InventoryManager.Instance()->GetInventoryItemCount(1), Gil = InventoryManager.Instance()->GetInventoryItemCount(1),
MGP = InventoryManager.Instance()->GetInventoryItemCount(29),
}; };
if (_msqQuests != null) if (_msqQuests != null)
@ -246,8 +259,8 @@ internal sealed class LocalStatsCalculator : IDisposable
private unsafe List<short> ExtractClassJobLevels(PlayerState* playerState) private unsafe List<short> ExtractClassJobLevels(PlayerState* playerState)
{ {
List<short> levels = new(); List<short> levels = new();
for (int i = 0; i < 32; ++i) for (int i = 0; i < 30; ++i)
levels.Add(playerState->ClassJobLevels[i]); levels.Add(playerState->ClassJobLevelArray[i]);
return levels; return levels;
} }

View File

@ -2,10 +2,10 @@
namespace Influx.LocalStatistics; namespace Influx.LocalStatistics;
public sealed class QuestInfo public class QuestInfo
{ {
public required uint RowId { get; init; } public required uint RowId { get; init; }
public required string Name { get; init; } public required string Name { get; init; }
public required IList<uint> PreviousQuestIds { get; init; } public required List<uint> PreviousQuestIds { get; init; }
public required uint Genre { get; init; } public required uint Genre { get; init; }
} }

View File

@ -8,8 +8,6 @@ namespace Influx;
internal sealed class StatisticsUpdate internal sealed class StatisticsUpdate
{ {
public required IReadOnlyDictionary<Character, Currencies> Currencies { get; init; } public required IReadOnlyDictionary<Character, Currencies> Currencies { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<SortingResult>> InventoryItems { get; init; } public required Dictionary<Character, List<SubmarineStats>> Submarines { get; init; }
public required IReadOnlyDictionary<Character, List<SubmarineStats>> Submarines { get; init; } public required Dictionary<Character, LocalStats> LocalStats { get; init; }
public required IReadOnlyDictionary<Character, LocalStats> LocalStats { get; init; }
public required IReadOnlyDictionary<ulong, FcStats> FcStats { get; init; }
} }

View File

@ -2,22 +2,21 @@
namespace Influx.SubmarineTracker; namespace Influx.SubmarineTracker;
internal sealed class Build public class Build
{ {
public Build(object @delegate) public Build(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate);
Type type = @delegate.GetType();
HullIdentifier = HullIdentifier =
(string)type.GetProperty("HullIdentifier")!.GetValue(@delegate)!; (string)@delegate.GetType().GetProperty("HullIdentifier")!.GetValue(@delegate)!;
SternIdentifier = SternIdentifier =
(string)type.GetProperty("SternIdentifier")!.GetValue(@delegate)!; (string)@delegate.GetType().GetProperty("SternIdentifier")!.GetValue(@delegate)!;
BowIdentifier = BowIdentifier =
(string)type.GetProperty("BowIdentifier")!.GetValue(@delegate)!; (string)@delegate.GetType().GetProperty("BowIdentifier")!.GetValue(@delegate)!;
BridgeIdentifier = BridgeIdentifier =
(string)type.GetProperty("BridgeIdentifier")!.GetValue(@delegate)!; (string)@delegate.GetType().GetProperty("BridgeIdentifier")!.GetValue(@delegate)!;
FullIdentifier = FullIdentifier =
(string)type.GetMethod("FullIdentifier")!.Invoke(@delegate, Array.Empty<object>())!; (string)@delegate.GetType().GetMethod("FullIdentifier")!.Invoke(@delegate, Array.Empty<object>())!;
} }
public string HullIdentifier { get; } public string HullIdentifier { get; }

View File

@ -1,8 +0,0 @@
namespace Influx.SubmarineTracker;
public enum EState
{
NoVoyage,
Returned,
Voyage,
}

View File

@ -0,0 +1,21 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Influx.SubmarineTracker;
public sealed class FcSubmarines
{
private readonly object _delegate;
public FcSubmarines(object @delegate)
{
_delegate = @delegate;
Submarines = ((IEnumerable)_delegate.GetType().GetField("Submarines")!.GetValue(_delegate)!)
.Cast<object>()
.Select(x => new Submarine(x))
.ToList();
}
public List<Submarine> Submarines { get; }
}

View File

@ -1,47 +1,15 @@
using System; namespace Influx.SubmarineTracker;
namespace Influx.SubmarineTracker; public sealed class Submarine
internal sealed class Submarine
{ {
public Submarine(object @delegate) public Submarine(object @delegate)
{ {
ArgumentNullException.ThrowIfNull(@delegate); Name = (string)@delegate.GetType().GetProperty("Name")!.GetValue(@delegate)!;
Type type = @delegate.GetType(); Level = (ushort)@delegate.GetType().GetProperty("Rank")!.GetValue(@delegate)!;
FreeCompanyId = (ulong)type.GetField("FreeCompanyId")!.GetValue(@delegate)!; Build = new Build(@delegate.GetType().GetProperty("Build")!.GetValue(@delegate)!);
Name = (string)type.GetField("Name")!.GetValue(@delegate)!;
Level = (ushort)type.GetField("Rank")!.GetValue(@delegate)!;
Build = new Build(type.GetProperty("Build")!.GetValue(@delegate)!);
ReturnTime = (DateTime)type.GetField("ReturnTime")!.GetValue(@delegate)!;
try
{
bool onVoyage = (bool)type.GetMethod("IsOnVoyage")!.Invoke(@delegate, Array.Empty<object>())!;
bool returned = (bool)type.GetMethod("IsDone")!.Invoke(@delegate, Array.Empty<object>())!;
if (onVoyage)
State = returned ? EState.Returned : EState.Voyage;
else
State = EState.NoVoyage;
if (State == EState.NoVoyage)
PredictedLevel = Level;
else
{
(uint predictedLevel, double _) = ((uint, double))type.GetMethod("PredictExpGrowth")!.Invoke(@delegate, Array.Empty<object?>())!;
PredictedLevel = (ushort)predictedLevel;
}
}
catch (Exception)
{
PredictedLevel = Level;
}
} }
public ulong FreeCompanyId { get; }
public string Name { get; } public string Name { get; }
public ushort Level { get; } public ushort Level { get; }
public ushort PredictedLevel { get; }
public Build Build { get; } public Build Build { get; }
public DateTime ReturnTime { get; }
public EState State { get; }
} }

View File

@ -1,21 +1,14 @@
using System; namespace Influx.SubmarineTracker;
namespace Influx.SubmarineTracker; public sealed class SubmarineStats
internal sealed class SubmarineStats
{ {
public required string Name { get; init; } public required string Name { get; init; }
public required int Id { get; init; } public required int Id { get; init; }
public required uint WorldId { get; init; }
public bool Enabled { get; set; } = true;
public required ushort Level { get; init; } public required ushort Level { get; init; }
public required ushort PredictedLevel { get; init; }
public required string Hull { get; init; } public required string Hull { get; init; }
public required string Stern { get; init; } public required string Stern { get; init; }
public required string Bow { get; init; } public required string Bow { get; init; }
public required string Bridge { get; init; } public required string Bridge { get; init; }
public required string Build { get; init; } public required string Build { get; init; }
public required EState State { get; init; }
public required DateTime ReturnTime { get; init; }
} }

View File

@ -1,10 +1,6 @@
using System; using System.Collections;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection;
using Dalamud.Plugin;
using Influx.AllaganTools; using Influx.AllaganTools;
using LLib; using LLib;
@ -19,56 +15,45 @@ internal sealed class SubmarineTrackerIpc
_dalamudReflector = dalamudReflector; _dalamudReflector = dalamudReflector;
} }
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] public Dictionary<Character, List<SubmarineStats>> GetSubmarineStats(List<Character> characters)
public IReadOnlyDictionary<Character, List<SubmarineStats>> GetSubmarineStats(List<Character> characters)
{ {
if (_dalamudReflector.TryGetDalamudPlugin("Submarine Tracker", out IDalamudPlugin? it, false, true)) if (_dalamudReflector.TryGetDalamudPlugin("Submarine Tracker", out var it, false, true))
{ {
var databaseCache = it.GetType() var submarineData = it.GetType().Assembly.GetType("SubmarineTracker.Data.Submarines");
.GetField("DatabaseCache", BindingFlags.Static | BindingFlags.Public)! var knownSubmarineData = submarineData!.GetField("KnownSubmarines")!;
.GetValue(null)!; return ((IEnumerable)knownSubmarineData.GetValue(null)!).Cast<object>()
var getSubmarines = databaseCache.GetType() .Select(x => new
.GetMethod("GetSubmarines", [])!; {
var knownSubmarineData = ((IEnumerable)getSubmarines.Invoke(databaseCache, [])!).Cast<object>(); OwnerId = (ulong)x.GetType().GetProperty("Key")!.GetValue(x)!,
return knownSubmarineData FcWrapper = x.GetType().GetProperty("Value")!.GetValue(x)!
.Select(x => new Submarine(x)) })
.GroupBy(x => x.FreeCompanyId) .Select(x => new
.Select(x => new SubmarineInfo( {
characters.SingleOrDefault(y => Owner = characters.FirstOrDefault(y => y.CharacterId == x.OwnerId),
y.CharacterType == CharacterType.FreeCompanyChest && y.CharacterId == x.Key), Subs = new FcSubmarines(x.FcWrapper).Submarines,
x.ToList() })
)) .Where(x => x.Owner != null)
.Select(x => new
{
x.Subs,
Fc = characters.FirstOrDefault(y => y.CharacterId == x.Owner!.FreeCompanyId)
})
.Where(x => x.Fc != null) .Where(x => x.Fc != null)
.ToDictionary(x => x.Fc!, x => x.Subs); .ToDictionary(
x => x.Fc!,
x => x.Subs.Select(y => new SubmarineStats
{
Id = x.Subs.IndexOf(y),
Name = y.Name,
Level = y.Level,
Hull = y.Build.HullIdentifier,
Stern = y.Build.SternIdentifier,
Bow = y.Build.BowIdentifier,
Bridge = y.Build.BridgeIdentifier,
Build = y.Build.FullIdentifier,
}).ToList());
} }
else else
return new Dictionary<Character, List<SubmarineStats>>(); return new Dictionary<Character, List<SubmarineStats>>();
} }
private sealed record SubmarineInfo(Character? Fc, List<SubmarineStats> Subs)
{
public SubmarineInfo(Character? fc, List<Submarine> subs)
: this(fc, subs.Select(x => Convert(fc, subs.IndexOf(x), x)).ToList())
{
}
private static SubmarineStats Convert(Character? fc, int index, Submarine y)
{
return new SubmarineStats
{
Id = index,
Name = y.Name,
WorldId = fc?.WorldId ?? 0,
Level = y.Level,
PredictedLevel = y.PredictedLevel,
Hull = y.Build.HullIdentifier,
Stern = y.Build.SternIdentifier,
Bow = y.Build.BowIdentifier,
Bridge = y.Build.BridgeIdentifier,
Build = y.Build.FullIdentifier,
State = y.State,
ReturnTime = y.ReturnTime,
};
}
}
} }

View File

@ -1,259 +1,111 @@
using System; using System.Linq;
using System.Linq;
using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiNET; using ImGuiNET;
using Influx.AllaganTools;
namespace Influx.Windows; namespace Influx.Windows;
internal sealed class ConfigurationWindow : Window internal sealed class ConfigurationWindow : Window
{ {
private readonly IDalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly AllaganToolsIpc _allaganToolsIpc;
private string[] _filterNames = [];
private int _filterIndexToAdd;
public ConfigurationWindow(IDalamudPluginInterface pluginInterface, IClientState clientState, public ConfigurationWindow(DalamudPluginInterface pluginInterface, IClientState clientState,
Configuration configuration, AllaganToolsIpc allaganToolsIpc) Configuration configuration)
: base("Configuration###InfluxConfiguration") : base("Configuration###InfluxConfiguration")
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_clientState = clientState; _clientState = clientState;
_configuration = configuration; _configuration = configuration;
_allaganToolsIpc = allaganToolsIpc;
} }
public event EventHandler? ConfigUpdated;
public override void Draw() public override void Draw()
{ {
using var tabBar = ImRaii.TabBar("InfluxConfigTabs"); if (ImGui.BeginTabBar("InfluxConfigTabs"))
if (tabBar)
{ {
DrawConnectionSettings();
DrawIncludedCharacters(); DrawIncludedCharacters();
DrawAllaganToolsFilters();
}
}
public override void OnOpen() => RefreshFilters(); ImGui.EndTabBar();
private void RefreshFilters()
{
_filterNames = _allaganToolsIpc.GetSearchFilters()
.Select(x => x.Value)
.Order()
.ToArray();
_filterIndexToAdd = 0;
}
private void DrawConnectionSettings()
{
using var tabItem = ImRaii.TabItem("Connection Settings");
if (!tabItem)
return;
bool enabled = _configuration.Server.Enabled;
if (ImGui.Checkbox("Enable Server Connection", ref enabled))
{
_configuration.Server.Enabled = enabled;
Save(true);
}
string server = _configuration.Server.Server;
if (ImGui.InputText("InfluxDB URL", ref server, 128))
{
_configuration.Server.Server = server;
Save(true);
}
string token = _configuration.Server.Token;
if (ImGui.InputText("Token", ref token, 128, ImGuiInputTextFlags.Password))
{
_configuration.Server.Token = token;
Save(true);
}
string organization = _configuration.Server.Organization;
if (ImGui.InputText("Organization", ref organization, 128))
{
_configuration.Server.Organization = organization;
Save(true);
}
string bucket = _configuration.Server.Bucket;
if (ImGui.InputText("Bucket", ref bucket, 128))
{
_configuration.Server.Bucket = bucket;
Save(true);
} }
} }
private void DrawIncludedCharacters() private void DrawIncludedCharacters()
{ {
using var tabItem = ImRaii.TabItem("Included Characters"); if (ImGui.BeginTabItem("Included Characters"))
if (!tabItem)
return;
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0, LocalPlayer.HomeWorld: not null })
{ {
string worldName = _clientState.LocalPlayer?.HomeWorld.GameData?.Name ?? "??"; if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
ImGui.TextWrapped(
$"Current Character: {_clientState.LocalPlayer?.Name} @ {worldName} ({_clientState.LocalContentId:X})");
ImGui.Indent(30);
Configuration.CharacterInfo? includedCharacter =
_configuration.IncludedCharacters.FirstOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (includedCharacter != null)
{ {
ImGui.TextColored(ImGuiColors.HealerGreen, "This character is currently included."); string worldName = _clientState.LocalPlayer?.HomeWorld.GameData?.Name ?? "??";
ImGui.TextWrapped(
bool includeFreeCompany = includedCharacter.IncludeFreeCompany; $"Current Character: {_clientState.LocalPlayer?.Name} @ {worldName} ({_clientState.LocalContentId:X})");
if (ImGui.Checkbox("Include Free Company statistics", ref includeFreeCompany)) ImGui.Indent(30);
if (_configuration.IncludedCharacters.Any(x => x.LocalContentId == _clientState.LocalContentId))
{ {
includedCharacter.IncludeFreeCompany = includeFreeCompany; ImGui.TextColored(ImGuiColors.HealerGreen, "This character is currently included.");
Save(); if (ImGui.Button("Remove inclusion"))
{
_configuration.IncludedCharacters.RemoveAll(
c => c.LocalContentId == _clientState.LocalContentId);
Save();
}
}
else
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This character is currently excluded.");
if (ImGui.Button("Include current character"))
{
_configuration.IncludedCharacters.Add(new Configuration.CharacterInfo
{
LocalContentId = _clientState.LocalContentId,
CachedPlayerName = _clientState.LocalPlayer?.Name.ToString() ?? "??",
CachedWorldName = worldName,
});
Save();
}
} }
ImGui.Spacing(); ImGui.Unindent(30);
if (ImGui.Button("Remove inclusion"))
{
var characterInfo =
_configuration.IncludedCharacters.First(c => c.LocalContentId == _clientState.LocalContentId);
_configuration.IncludedCharacters.Remove(characterInfo);
Save();
}
} }
else else
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, ImGui.TextColored(ImGuiColors.DalamudRed, "You are not logged in.");
"This character is currently excluded.");
if (ImGui.Button("Include current character"))
{
_configuration.IncludedCharacters.Add(new Configuration.CharacterInfo
{
LocalContentId = _clientState.LocalContentId,
CachedPlayerName = _clientState.LocalPlayer?.Name.ToString() ?? "??",
CachedWorldName = worldName,
});
Save();
}
} }
ImGui.Unindent(30); ImGui.Separator();
} ImGui.TextWrapped("Characters that are included:");
else ImGui.Spacing();
{
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not logged in.");
}
ImGui.Separator(); if (_configuration.IncludedCharacters.Count == 0)
ImGui.TextWrapped("Characters that are included:");
ImGui.Spacing();
if (_configuration.IncludedCharacters.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "No included characters.");
}
else
{
foreach (var world in _configuration.IncludedCharacters.OrderBy(x => x.CachedWorldName)
.ThenBy(x => x.LocalContentId).GroupBy(x => x.CachedWorldName))
{ {
if (ImGui.CollapsingHeader($"{world.Key} ({world.Count()})##World{world.Key}", ImGui.TextColored(ImGuiColors.DalamudGrey, "No included characters.");
ImGuiTreeNodeFlags.DefaultOpen)) }
else
{
foreach (var world in _configuration.IncludedCharacters.OrderBy(x => x.CachedWorldName).ThenBy(x => x.LocalContentId).GroupBy(x => x.CachedWorldName))
{ {
ImGui.CollapsingHeader($"{world.Key} ({world.Count()})", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.Bullet);
ImGui.Indent(30); ImGui.Indent(30);
foreach (var characterInfo in world) foreach (var characterInfo in world)
{ {
ImGui.Selectable( ImGui.Selectable(
$"{characterInfo.CachedPlayerName} @ {characterInfo.CachedWorldName} ({characterInfo.LocalContentId:X}{(!characterInfo.IncludeFreeCompany ? ", no FC" : "")})"); $"{characterInfo.CachedPlayerName} @ {characterInfo.CachedWorldName} ({characterInfo.LocalContentId:X})");
} }
ImGui.Unindent(30); ImGui.Unindent(30);
} }
} }
ImGui.EndTabItem();
} }
} }
private void DrawAllaganToolsFilters() private void Save()
{
using var tabItem = ImRaii.TabItem("Inventory Filters");
if (!tabItem)
return;
if (_configuration.IncludedInventoryFilters.Count > 0)
{
int? indexToRemove = null;
ImGui.Text("Selected Filters:");
ImGui.Indent(30);
foreach (var filter in _configuration.IncludedInventoryFilters)
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, $"{filter.Name}"))
{
indexToRemove = _configuration.IncludedInventoryFilters.IndexOf(filter);
}
}
ImGui.Unindent(30);
if (indexToRemove != null)
{
_configuration.IncludedInventoryFilters.RemoveAt(indexToRemove.Value);
Save();
}
}
else
{
ImGui.Text("You are not tracking any AllaganTools filters.");
}
ImGui.Separator();
if (_filterNames.Length > 0)
{
ImGui.Combo("Add Search Filter", ref _filterIndexToAdd, _filterNames, _filterNames.Length);
ImGui.BeginDisabled(
_configuration.IncludedInventoryFilters.Any(x => x.Name == _filterNames[_filterIndexToAdd]));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Track Filter"))
{
_configuration.IncludedInventoryFilters.Add(new Configuration.FilterInfo
{
Name = _filterNames[_filterIndexToAdd],
});
Save();
}
ImGui.EndDisabled();
}
else
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"You don't have any search filters, or the AllaganTools integration doesn't work.");
}
ImGui.Separator();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Sync, "Refresh Filters"))
RefreshFilters();
}
private void Save(bool sendEvent = false)
{ {
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
if (sendEvent)
ConfigUpdated?.Invoke(this, EventArgs.Empty);
} }
} }

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
@ -24,10 +23,10 @@ internal sealed class StatisticsWindow : Window
public override void Draw() public override void Draw()
{ {
if (ImGui.BeginTable("Currencies###InfluxStatisticsCurrencies", 2)) if (ImGui.BeginTable("Currencies###InfluxStatisticsCurrencies", 4))
{ {
ImGui.TableSetupColumn("Name"); ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn($"Gil ({_rows.Sum(x => x.Gil):N0})##Gil"); ImGui.TableSetupColumn("Gil");
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
foreach (var row in _rows) foreach (var row in _rows)
@ -37,7 +36,7 @@ internal sealed class StatisticsWindow : Window
ImGui.Text(row.Name); ImGui.Text(row.Name);
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
ImGui.Text(row.Gil.ToString("N0", CultureInfo.InvariantCulture)); ImGui.Text(row.Gil.ToString("N0"));
} }
ImGui.EndTable(); ImGui.EndTable();

View File

@ -1,56 +1,28 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net8.0-windows7.0": { "net7.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.13, )", "requested": "[2.1.12, )",
"resolved": "2.1.13", "resolved": "2.1.12",
"contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ==" "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
}, },
"InfluxDB.Client": { "InfluxDB.Client": {
"type": "Direct", "type": "Direct",
"requested": "[4.14.0, )", "requested": "[4.13.0, )",
"resolved": "4.14.0", "resolved": "4.13.0",
"contentHash": "X42YEtBon4thG3qsbRWxO6VAadnX4CBn1xLhNYZGNizUMUboyut6VlRqa5BGiU1pHe4MCP+cds9rD4nWGB7vsQ==", "contentHash": "2kDPC//sbSjm6R8lxqJrufNfF1AMhmeVw7dBnrOsMQjjxvaPrWFkITWIlL8AVtg5TdTqah9APp1laaJye6ebFg==",
"dependencies": { "dependencies": {
"InfluxDB.Client.Core": "4.14.0", "InfluxDB.Client.Core": "4.13.0",
"JsonSubTypes": "2.0.1", "JsonSubTypes": "2.0.1",
"Microsoft.Extensions.ObjectPool": "7.0.13", "Microsoft.Extensions.ObjectPool": "7.0.9",
"Microsoft.Net.Http.Headers": "2.2.8", "Microsoft.Net.Http.Headers": "2.2.8",
"System.Collections.Immutable": "6.0.0", "System.Collections.Immutable": "6.0.0",
"System.Configuration.ConfigurationManager": "6.0.1", "System.Configuration.ConfigurationManager": "6.0.1",
"System.Reactive": "6.0.0" "System.Reactive": "6.0.0"
} }
}, },
"Microsoft.Extensions.ObjectPool": {
"type": "Direct",
"requested": "[9.0.0-preview.1.24081.5, )",
"resolved": "9.0.0-preview.1.24081.5",
"contentHash": "aAR7YW+pUUdvHk3vj7GtAi71dWGDIuY9270lsmQ6lKw23zzY+r8pLP3cGNbJdlnA9VWl+S+gnIVkBCqj2ROlEg=="
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"CsvHelper": { "CsvHelper": {
"type": "Transitive", "type": "Transitive",
"resolved": "30.0.1", "resolved": "30.0.1",
@ -58,13 +30,13 @@
}, },
"InfluxDB.Client.Core": { "InfluxDB.Client.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.14.0", "resolved": "4.13.0",
"contentHash": "PtySdJE39Tv1T8p9SSpJve0Vm2gBzLdsHRL/weA97fEkeMip6Iw6zdgvb4ANWJ0MbYbprQQkSaZj2e4pTJpO6Q==", "contentHash": "SS6kVhUuQTjpbCBn6ULgXz/tLSttZhMEhmr0Vxk3vnvtlce4mYjX48NLk8QHWdUEyTL195AGrSyZTqYaDTZrgA==",
"dependencies": { "dependencies": {
"CsvHelper": "30.0.1", "CsvHelper": "30.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"NodaTime": "3.1.9", "NodaTime": "3.1.9",
"NodaTime.Serialization.JsonNet": "3.1.0", "NodaTime.Serialization.JsonNet": "3.0.1",
"RestSharp": "110.1.0" "RestSharp": "110.1.0"
} }
}, },
@ -76,10 +48,10 @@
"Newtonsoft.Json": "13.0.1" "Newtonsoft.Json": "13.0.1"
} }
}, },
"Microsoft.Build.Tasks.Git": { "Microsoft.Extensions.ObjectPool": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.9",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" "contentHash": "fFYlvFV8gle6IJ7Z0C3IydLfrzruL82pbBmn7oGsPK0zPtyN4pk2uX242ATOKbodZlRqLMAH6RE5wkRMCbkxug=="
}, },
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
@ -99,47 +71,6 @@
"System.Buffers": "4.5.0" "System.Buffers": "4.5.0"
} }
}, },
"Microsoft.SourceLink.AzureRepos.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.Win32.SystemEvents": { "Microsoft.Win32.SystemEvents": {
"type": "Transitive", "type": "Transitive",
"resolved": "6.0.0", "resolved": "6.0.0",
@ -160,10 +91,10 @@
}, },
"NodaTime.Serialization.JsonNet": { "NodaTime.Serialization.JsonNet": {
"type": "Transitive", "type": "Transitive",
"resolved": "3.1.0", "resolved": "3.0.1",
"contentHash": "eEr9lXUz50TYr4rpeJG4TDAABkpxjIKr5mDSi/Zav8d6Njy6fH7x4ZtNwWFj0Vd+vIvEZNrHFQ4Gfy8j4BqRGg==", "contentHash": "bmw+ElaOr21HZQQVYWZKgqs0QpSb/rJcJxem3Ok6YWAdenqOi0PS2hIkvlh+QJfsl2X3VJcla9Rhya/N3JnweA==",
"dependencies": { "dependencies": {
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.1",
"NodaTime": "[3.0.0, 4.0.0)" "NodaTime": "[3.0.0, 4.0.0)"
} }
}, },
@ -263,17 +194,14 @@
"autoretainerapi": { "autoretainerapi": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"ECommons": "[2.2.0.2, )" "ECommons": "[2.1.0, )"
} }
}, },
"ecommons": { "ecommons": {
"type": "Project" "type": "Project"
}, },
"llib": { "llib": {
"type": "Project", "type": "Project"
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
} }
} }
} }

2
LLib

@ -1 +1 @@
Subproject commit 8d947be6784804a7cab120d596dd54e88e548efc Subproject commit 2f6ef354c401a9f1bb9b807327acad7e98662a2e

View File

@ -1,7 +0,0 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
}
}