From 65c0bec80e18ea72771947c79734b178cf90860e Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 3 Jul 2024 01:03:00 +0200 Subject: [PATCH] API 10 --- .gitmodules | 3 + LLib | 1 + RetainerTrack.sln | 6 + .../Database/Compiled/PlayerEntityType.cs | 8 + .../RetainerTrackContextModelBuilder.cs | 12 ++ ...629073047_AddAccountIdToPlayer.Designer.cs | 66 ++++++++ .../20240629073047_AddAccountIdToPlayer.cs | 28 ++++ .../RetainerTrackContextModelSnapshot.cs | 4 + RetainerTrack/Database/Player.cs | 2 + RetainerTrack/Handlers/ContentIdToName.cs | 7 - RetainerTrack/Handlers/GameHooks.cs | 33 ++-- .../Handlers/MarketBoardOfferingsHandler.cs | 43 +---- .../Handlers/MarketBoardUIHandler.cs | 6 + RetainerTrack/Handlers/ObjectTableHandler.cs | 78 +++++++++ RetainerTrack/Handlers/PartyHandler.cs | 68 -------- RetainerTrack/Handlers/PersistenceContext.cs | 148 ++++++++++++++---- RetainerTrack/Handlers/PlayerMapping.cs | 8 + RetainerTrack/RetainerTrack.csproj | 60 +------ RetainerTrack/RetainerTrackPlugin.cs | 11 +- RetainerTrack/packages.lock.json | 74 ++++++++- 20 files changed, 450 insertions(+), 216 deletions(-) create mode 100644 .gitmodules create mode 160000 LLib create mode 100644 RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.Designer.cs create mode 100644 RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.cs delete mode 100644 RetainerTrack/Handlers/ContentIdToName.cs create mode 100644 RetainerTrack/Handlers/ObjectTableHandler.cs delete mode 100644 RetainerTrack/Handlers/PartyHandler.cs create mode 100644 RetainerTrack/Handlers/PlayerMapping.cs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c1039e4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LLib"] + path = LLib + url = https://git.carvel.li/liza/LLib diff --git a/LLib b/LLib new file mode 160000 index 0000000..7027d29 --- /dev/null +++ b/LLib @@ -0,0 +1 @@ +Subproject commit 7027d291efbbff6a55944dd521d3907210ddecbe diff --git a/RetainerTrack.sln b/RetainerTrack.sln index 9231cf7..31d015f 100644 --- a/RetainerTrack.sln +++ b/RetainerTrack.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RetainerTrack", "RetainerTrack\RetainerTrack.csproj", "{5FA75994-45B8-4E1A-A9D6-F28CD4C52342}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{3095F0BF-355A-4D13-BDDE-CCB4CA48D3C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {5FA75994-45B8-4E1A-A9D6-F28CD4C52342}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FA75994-45B8-4E1A-A9D6-F28CD4C52342}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FA75994-45B8-4E1A-A9D6-F28CD4C52342}.Release|Any CPU.Build.0 = Release|Any CPU + {3095F0BF-355A-4D13-BDDE-CCB4CA48D3C3}.Debug|Any CPU.ActiveCfg = Debug|x64 + {3095F0BF-355A-4D13-BDDE-CCB4CA48D3C3}.Debug|Any CPU.Build.0 = Debug|x64 + {3095F0BF-355A-4D13-BDDE-CCB4CA48D3C3}.Release|Any CPU.ActiveCfg = Debug|x64 + {3095F0BF-355A-4D13-BDDE-CCB4CA48D3C3}.Release|Any CPU.Build.0 = Debug|x64 EndGlobalSection EndGlobal diff --git a/RetainerTrack/Database/Compiled/PlayerEntityType.cs b/RetainerTrack/Database/Compiled/PlayerEntityType.cs index a312b24..03b7f03 100644 --- a/RetainerTrack/Database/Compiled/PlayerEntityType.cs +++ b/RetainerTrack/Database/Compiled/PlayerEntityType.cs @@ -28,6 +28,14 @@ namespace RetainerTrack.Database.Compiled sentinel: 0ul); localContentId.TypeMapping = SqliteULongTypeMapping.Default; + var accountId = runtimeEntityType.AddProperty( + "AccountId", + typeof(ulong?), + propertyInfo: typeof(Player).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(Player).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + accountId.TypeMapping = SqliteULongTypeMapping.Default; + var name = runtimeEntityType.AddProperty( "Name", typeof(string), diff --git a/RetainerTrack/Database/Compiled/RetainerTrackContextModelBuilder.cs b/RetainerTrack/Database/Compiled/RetainerTrackContextModelBuilder.cs index 9e0c24a..6f8d1ee 100644 --- a/RetainerTrack/Database/Compiled/RetainerTrackContextModelBuilder.cs +++ b/RetainerTrack/Database/Compiled/RetainerTrackContextModelBuilder.cs @@ -34,6 +34,11 @@ namespace RetainerTrack.Database.Compiled var defaultTableMappings = new List>(); player.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings); var retainerTrackDatabasePlayerTableBase = new TableBase("RetainerTrack.Database.Player", null, relationalModel); + var accountIdColumnBase = new ColumnBase("AccountId", "INTEGER", retainerTrackDatabasePlayerTableBase) + { + IsNullable = true + }; + retainerTrackDatabasePlayerTableBase.Columns.Add("AccountId", accountIdColumnBase); var localContentIdColumnBase = new ColumnBase("LocalContentId", "INTEGER", retainerTrackDatabasePlayerTableBase); retainerTrackDatabasePlayerTableBase.Columns.Add("LocalContentId", localContentIdColumnBase); var nameColumnBase = new ColumnBase("Name", "TEXT", retainerTrackDatabasePlayerTableBase); @@ -43,6 +48,7 @@ namespace RetainerTrack.Database.Compiled retainerTrackDatabasePlayerTableBase.AddTypeMapping(retainerTrackDatabasePlayerMappingBase, false); defaultTableMappings.Add(retainerTrackDatabasePlayerMappingBase); RelationalModel.CreateColumnMapping((ColumnBase)localContentIdColumnBase, player.FindProperty("LocalContentId")!, retainerTrackDatabasePlayerMappingBase); + RelationalModel.CreateColumnMapping((ColumnBase)accountIdColumnBase, player.FindProperty("AccountId")!, retainerTrackDatabasePlayerMappingBase); RelationalModel.CreateColumnMapping((ColumnBase)nameColumnBase, player.FindProperty("Name")!, retainerTrackDatabasePlayerMappingBase); var tableMappings = new List(); @@ -50,6 +56,11 @@ namespace RetainerTrack.Database.Compiled var playersTable = new Table("Players", null, relationalModel); var localContentIdColumn = new Column("LocalContentId", "INTEGER", playersTable); playersTable.Columns.Add("LocalContentId", localContentIdColumn); + var accountIdColumn = new Column("AccountId", "INTEGER", playersTable) + { + IsNullable = true + }; + playersTable.Columns.Add("AccountId", accountIdColumn); var nameColumn = new Column("Name", "TEXT", playersTable); playersTable.Columns.Add("Name", nameColumn); var pK_Players = new UniqueConstraint("PK_Players", playersTable, new[] { localContentIdColumn }); @@ -65,6 +76,7 @@ namespace RetainerTrack.Database.Compiled playersTable.AddTypeMapping(playersTableMapping, false); tableMappings.Add(playersTableMapping); RelationalModel.CreateColumnMapping(localContentIdColumn, player.FindProperty("LocalContentId")!, playersTableMapping); + RelationalModel.CreateColumnMapping(accountIdColumn, player.FindProperty("AccountId")!, playersTableMapping); RelationalModel.CreateColumnMapping(nameColumn, player.FindProperty("Name")!, playersTableMapping); var retainer = FindEntityType("RetainerTrack.Database.Retainer")!; diff --git a/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.Designer.cs b/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.Designer.cs new file mode 100644 index 0000000..ad91c9d --- /dev/null +++ b/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.Designer.cs @@ -0,0 +1,66 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RetainerTrack.Database; + +#nullable disable + +namespace RetainerTrack.Database.Migrations +{ + [DbContext(typeof(RetainerTrackContext))] + [Migration("20240629073047_AddAccountIdToPlayer")] + partial class AddAccountIdToPlayer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.5"); + + modelBuilder.Entity("RetainerTrack.Database.Player", b => + { + b.Property("LocalContentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("LocalContentId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RetainerTrack.Database.Retainer", b => + { + b.Property("LocalContentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("OwnerLocalContentId") + .HasColumnType("INTEGER"); + + b.Property("WorldId") + .HasColumnType("INTEGER"); + + b.HasKey("LocalContentId"); + + b.ToTable("Retainers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.cs b/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.cs new file mode 100644 index 0000000..581061f --- /dev/null +++ b/RetainerTrack/Database/Migrations/20240629073047_AddAccountIdToPlayer.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RetainerTrack.Database.Migrations +{ + /// + public partial class AddAccountIdToPlayer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccountId", + table: "Players", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AccountId", + table: "Players"); + } + } +} diff --git a/RetainerTrack/Database/Migrations/RetainerTrackContextModelSnapshot.cs b/RetainerTrack/Database/Migrations/RetainerTrackContextModelSnapshot.cs index eab2ab6..4fcc15b 100644 --- a/RetainerTrack/Database/Migrations/RetainerTrackContextModelSnapshot.cs +++ b/RetainerTrack/Database/Migrations/RetainerTrackContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -22,6 +23,9 @@ namespace RetainerTrack.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AccountId") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasMaxLength(20) diff --git a/RetainerTrack/Database/Player.cs b/RetainerTrack/Database/Player.cs index 77968be..8389784 100644 --- a/RetainerTrack/Database/Player.cs +++ b/RetainerTrack/Database/Player.cs @@ -9,4 +9,6 @@ public class Player [MaxLength(20), Required] public string? Name { get; set; } + + public ulong? AccountId { get; set; } } diff --git a/RetainerTrack/Handlers/ContentIdToName.cs b/RetainerTrack/Handlers/ContentIdToName.cs deleted file mode 100644 index a0d1fc1..0000000 --- a/RetainerTrack/Handlers/ContentIdToName.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace RetainerTrack.Handlers; - -internal sealed class ContentIdToName -{ - public ulong ContentId { get; init; } - public string PlayerName { get; init; } = string.Empty; -} diff --git a/RetainerTrack/Handlers/GameHooks.cs b/RetainerTrack/Handlers/GameHooks.cs index 2c1af26..c1325d7 100644 --- a/RetainerTrack/Handlers/GameHooks.cs +++ b/RetainerTrack/Handlers/GameHooks.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Dalamud.Hooking; using Dalamud.Memory; using Dalamud.Plugin.Services; +using Dalamud.Utility; using Dalamud.Utility.Signatures; using Microsoft.Extensions.Logging; @@ -36,7 +37,8 @@ internal sealed unsafe class GameHooks : IDisposable #pragma warning restore CS0649 - public GameHooks(ILogger logger, PersistenceContext persistenceContext, IGameInteropProvider gameInteropProvider) + public GameHooks(ILogger logger, PersistenceContext persistenceContext, + IGameInteropProvider gameInteropProvider) { _logger = logger; _persistenceContext = persistenceContext; @@ -53,9 +55,10 @@ internal sealed unsafe class GameHooks : IDisposable { try { - var mapping = new ContentIdToName + var mapping = new PlayerMapping { ContentId = contentId, + AccountId = null, PlayerName = MemoryHelper.ReadString(new nint(playerName), Encoding.ASCII, 32), }; @@ -63,7 +66,8 @@ internal sealed unsafe class GameHooks : IDisposable { _logger.LogTrace("Content id {ContentId} belongs to '{Name}'", mapping.ContentId, mapping.PlayerName); - Task.Run(() => _persistenceContext.HandleContentIdMapping(mapping)); + if (mapping.PlayerName.IsValidCharacterName()) + Task.Run(() => _persistenceContext.HandleContentIdMapping(new List { mapping })); } else { @@ -84,19 +88,23 @@ internal sealed unsafe class GameHooks : IDisposable try { var result = Marshal.PtrToStructure(dataPtr); - List mappings = new(); + List mappings = new(); foreach (SocialListPlayer player in result.PlayerSpan) { - var mapping = new ContentIdToName + if (player.ContentId == 0) + continue; + + var mapping = new PlayerMapping { ContentId = player.ContentId, + AccountId = player.AccountId != 0 ? player.AccountId : null, PlayerName = MemoryHelper.ReadString(new nint(player.CharacterName), Encoding.ASCII, 32), }; if (!string.IsNullOrEmpty(mapping.PlayerName)) { - _logger.LogDebug("Content id {ContentId} belongs to '{Name}'", mapping.ContentId, - mapping.PlayerName); + _logger.LogDebug("Content id {ContentId} belongs to '{Name}' ({AccountId})", mapping.ContentId, + mapping.PlayerName, mapping.AccountId); mappings.Add(mapping); } else @@ -139,12 +147,12 @@ internal sealed unsafe class GameHooks : IDisposable [StructLayout(LayoutKind.Explicit, Size = 0x420)] internal struct SocialListResultPage { - [FieldOffset(0x10)] private fixed byte Players[10 * 0x58]; + [FieldOffset(0x10)] private fixed byte Players[10 * 0x70]; public Span PlayerSpan => new(Unsafe.AsPointer(ref Players[0]), 10); } - [StructLayout(LayoutKind.Explicit, Size = 0x68, Pack = 1)] + [StructLayout(LayoutKind.Explicit, Size = 0x70, Pack = 1)] internal struct SocialListPlayer { /// @@ -153,9 +161,14 @@ internal sealed unsafe class GameHooks : IDisposable /// [FieldOffset(0x00)] public readonly ulong ContentId; + /// + /// Only seems to be set for certain kind of social lists, e.g. friend list/FC members doesn't include any. + /// + [FieldOffset(0x18)] public readonly ulong AccountId; + /// /// This *can* be empty, e.g. if you're querying your friend list, the names are ONLY set for characters on the same world. /// - [FieldOffset(0x3C)] public fixed byte CharacterName[32]; + [FieldOffset(0x44)] public fixed byte CharacterName[32]; } } diff --git a/RetainerTrack/Handlers/MarketBoardOfferingsHandler.cs b/RetainerTrack/Handlers/MarketBoardOfferingsHandler.cs index e35b5a4..5d72df1 100644 --- a/RetainerTrack/Handlers/MarketBoardOfferingsHandler.cs +++ b/RetainerTrack/Handlers/MarketBoardOfferingsHandler.cs @@ -1,64 +1,38 @@ using System; using System.Threading.Tasks; using Dalamud.Game.Network.Structures; -using Dalamud.Hooking; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI.Info; using Microsoft.Extensions.Logging; namespace RetainerTrack.Handlers; internal sealed class MarketBoardOfferingsHandler : IDisposable { - private unsafe delegate void* MarketBoardOfferings(InfoProxyItemSearch* a1, nint packetData); - + private readonly IMarketBoard _marketBoard; private readonly ILogger _logger; private readonly IClientState _clientState; private readonly PersistenceContext _persistenceContext; - private readonly Hook _marketBoardOfferingsHook; - public unsafe MarketBoardOfferingsHandler( + public MarketBoardOfferingsHandler( + IMarketBoard marketBoard, ILogger logger, IClientState clientState, - IGameInteropProvider gameInteropProvider, PersistenceContext persistenceContext) { + _marketBoard = marketBoard; _logger = logger; _clientState = clientState; _persistenceContext = persistenceContext; - _logger.LogDebug("Setting up offerings hook"); - _marketBoardOfferingsHook = - gameInteropProvider.HookFromSignature("48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 82 ?? ?? ?? ?? 48 8B FA 48 8B D9 38 41 19 74 54", - MarketBoardOfferingsDetour); - _marketBoardOfferingsHook.Enable(); - _logger.LogDebug("Offerings hook enabled successfully"); + _marketBoard.OfferingsReceived += HandleOfferings; } public void Dispose() { - _marketBoardOfferingsHook.Dispose(); + _marketBoard.OfferingsReceived += HandleOfferings; } - // adapted from https://github.com/tesu/PennyPincher/commit/0f9b3963fd4a6e9b87f585ee491d4de59a93f7a3 - private unsafe void* MarketBoardOfferingsDetour(InfoProxyItemSearch* a1, nint packetData) - { - try - { - if (packetData != nint.Zero) - { - ParseOfferings(packetData); - } - } - catch (Exception e) - { - _logger.LogError(e, "Could not parse marketboard offerings."); - } - - return _marketBoardOfferingsHook.Original(a1, packetData); - } - - private void ParseOfferings(nint dataPtr) + private void HandleOfferings(IMarketBoardCurrentOfferings currentOfferings) { ushort worldId = (ushort?)_clientState.LocalPlayer?.CurrentWorld.Id ?? 0; if (worldId == 0) @@ -67,7 +41,6 @@ internal sealed class MarketBoardOfferingsHandler : IDisposable return; } - var listings = MarketBoardCurrentOfferings.Read(dataPtr); - Task.Run(() => _persistenceContext.HandleMarketBoardPage(listings, worldId)); + Task.Run(() => _persistenceContext.HandleMarketBoardPage(currentOfferings, worldId)); } } diff --git a/RetainerTrack/Handlers/MarketBoardUIHandler.cs b/RetainerTrack/Handlers/MarketBoardUIHandler.cs index cca9b51..0046d88 100644 --- a/RetainerTrack/Handlers/MarketBoardUIHandler.cs +++ b/RetainerTrack/Handlers/MarketBoardUIHandler.cs @@ -51,11 +51,17 @@ internal sealed unsafe class MarketBoardUiHandler : IDisposable for (int i = 0; i < length; ++i) { var listItem = results->ItemRendererList[i].AtkComponentListItemRenderer; + if (listItem == null) + return; + var uldManager = listItem->AtkComponentButton.AtkComponentBase.UldManager; if (uldManager.NodeListCount < 14) continue; var retainerNameNode = (AtkTextNode*)uldManager.NodeList[5]; + if (retainerNameNode == null) + return; + string retainerName = retainerNameNode->NodeText.ToString(); if (!retainerName.Contains('(', StringComparison.Ordinal)) { diff --git a/RetainerTrack/Handlers/ObjectTableHandler.cs b/RetainerTrack/Handlers/ObjectTableHandler.cs new file mode 100644 index 0000000..00c2c0c --- /dev/null +++ b/RetainerTrack/Handlers/ObjectTableHandler.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Microsoft.Extensions.Logging; + +namespace RetainerTrack.Handlers; + +internal sealed class ObjectTableHandler : IDisposable +{ + private readonly IObjectTable _objectTable; + private readonly IFramework _framework; + private readonly IClientState _clientState; + private readonly ILogger _logger; + private readonly PersistenceContext _persistenceContext; + + private long _lastUpdate; + + public ObjectTableHandler(IObjectTable objectTable, IFramework framework, IClientState clientState, ILogger logger, PersistenceContext persistenceContext) + { + _objectTable = objectTable; + _framework = framework; + _clientState = clientState; + _logger = logger; + _persistenceContext = persistenceContext; + + _framework.Update += FrameworkUpdate; + } + + private unsafe void FrameworkUpdate(IFramework framework) + { + long now = Environment.TickCount64; + if (!_clientState.IsLoggedIn || _clientState.IsPvPExcludingDen || now - _lastUpdate < 30_000) + return; + + _lastUpdate = now; + + List playerMappings = new(); + foreach (var obj in _objectTable) + { + if (obj.ObjectKind == ObjectKind.Player) + { + var bc = (BattleChara*)obj.Address; + var ep = (ExtendedPlayer*)obj.Address; + + if (ep->ContentId == 0 || ep->AccountId == 0) + continue; + + playerMappings.Add(new PlayerMapping + { + ContentId = ep->ContentId, + AccountId = ep->AccountId, + PlayerName = bc->NameString, + }); + } + } + + if (playerMappings.Count > 0) + _persistenceContext.HandleContentIdMapping(playerMappings); + } + + public void Dispose() + { + _framework.Update -= FrameworkUpdate; + } + + [StructLayout(LayoutKind.Explicit, Size = 0x2280)] + public struct ExtendedPlayer + { + [FieldOffset(0x2258)] public ulong AccountId; + [FieldOffset(0x2260)] public ulong ContentId; + } +} diff --git a/RetainerTrack/Handlers/PartyHandler.cs b/RetainerTrack/Handlers/PartyHandler.cs deleted file mode 100644 index d079956..0000000 --- a/RetainerTrack/Handlers/PartyHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Dalamud.Memory; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Group; - -namespace RetainerTrack.Handlers; - -internal sealed class PartyHandler : IDisposable -{ - private readonly IFramework _framework; - private readonly IClientState _clientState; - private readonly PersistenceContext _persistenceContext; - - private long _lastUpdate; - - public PartyHandler(IFramework framework, IClientState clientState, PersistenceContext persistenceContext) - { - _framework = framework; - _clientState = clientState; - _persistenceContext = persistenceContext; - - _framework.Update += FrameworkUpdate; - } - - private unsafe void FrameworkUpdate(IFramework _) - { - long now = Environment.TickCount64; - if (!_clientState.IsLoggedIn || _clientState.IsPvPExcludingDen || now - _lastUpdate < 180_000) - return; - - _lastUpdate = now; - - // skip if we're not in an alliance, party members are handled via social list updates - var groupManager = GroupManager.Instance(); - if (groupManager->AllianceFlags == 0x0) - return; - - List mappings = new(); - foreach (var allianceMember in groupManager->AllianceMembersSpan) - HandlePartyMember(allianceMember, mappings); - - if (mappings.Count > 0) - Task.Run(() => _persistenceContext.HandleContentIdMapping(mappings)); - } - - private static unsafe void HandlePartyMember(PartyMember partyMember, List contentIdToNames) - { - if (partyMember.ContentID == 0) - return; - - string name = MemoryHelper.ReadStringNullTerminated((nint)partyMember.Name); - if (string.IsNullOrEmpty(name)) - return; - - contentIdToNames.Add(new ContentIdToName - { - ContentId = (ulong)partyMember.ContentID, - PlayerName = name, - }); - } - - public void Dispose() - { - _framework.Update -= FrameworkUpdate; - } -} diff --git a/RetainerTrack/Handlers/PersistenceContext.cs b/RetainerTrack/Handlers/PersistenceContext.cs index a67ed91..c559487 100644 --- a/RetainerTrack/Handlers/PersistenceContext.cs +++ b/RetainerTrack/Handlers/PersistenceContext.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Dalamud.Game.Network.Structures; using Dalamud.Plugin.Services; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RetainerTrack.Database; @@ -17,7 +18,7 @@ internal sealed class PersistenceContext private readonly IClientState _clientState; private readonly IServiceProvider _serviceProvider; private readonly ConcurrentDictionary> _worldRetainerCache = new(); - private readonly ConcurrentDictionary _playerNameCache = new(); + private readonly ConcurrentDictionary _playerCache = new(); public PersistenceContext(ILogger logger, IClientState clientState, IServiceProvider serviceProvider) @@ -42,7 +43,13 @@ internal sealed class PersistenceContext } foreach (var player in dbContext.Players) - _playerNameCache[player.LocalContentId] = player.Name ?? string.Empty; + { + _playerCache[player.LocalContentId] = new CachedPlayer + { + AccountId = player.AccountId, + Name = player.Name ?? string.Empty, + }; + } } } @@ -56,7 +63,9 @@ internal sealed class PersistenceContext if (!currentWorldCache.TryGetValue(retainerName, out ulong playerContentId)) return string.Empty; - return _playerNameCache.TryGetValue(playerContentId, out string? playerName) ? playerName : string.Empty; + return _playerCache.TryGetValue(playerContentId, out CachedPlayer? cachedPlayer) + ? cachedPlayer.Name + : string.Empty; } public IReadOnlyList GetRetainerNamesForCharacter(string characterName, uint world) @@ -73,12 +82,14 @@ internal sealed class PersistenceContext .AsReadOnly(); } - public void HandleMarketBoardPage(MarketBoardCurrentOfferings listings, ushort worldId) + public void HandleMarketBoardPage(IMarketBoardCurrentOfferings currentOfferings, ushort worldId) { try { var updates = - listings.ItemListings.DistinctBy(o => o.RetainerId) + currentOfferings.ItemListings + .Cast() + .DistinctBy(o => o.RetainerId) .Where(l => l.RetainerId != 0) .Where(l => l.RetainerOwnerId != 0) .Select(l => @@ -111,7 +122,8 @@ internal sealed class PersistenceContext Retainer? dbRetainer = dbContext.Retainers.Find(retainer.LocalContentId); if (dbRetainer != null) { - _logger.LogDebug("Updating retainer {RetainerName} with {LocalContentId}", retainer.Name, retainer.LocalContentId); + _logger.LogDebug("Updating retainer {RetainerName} with {LocalContentId}", retainer.Name, + retainer.LocalContentId); dbRetainer.Name = retainer.Name; dbRetainer.WorldId = retainer.WorldId; dbRetainer.OwnerLocalContentId = retainer.OwnerLocalContentId; @@ -119,11 +131,15 @@ internal sealed class PersistenceContext } else { - _logger.LogDebug("Adding retainer {RetainerName} with {LocalContentId}", retainer.Name, retainer.LocalContentId); + _logger.LogDebug("Adding retainer {RetainerName} with {LocalContentId}", retainer.Name, + retainer.LocalContentId); dbContext.Retainers.Add(retainer); } - if (!_playerNameCache.TryGetValue(retainer.OwnerLocalContentId, out string? ownerName)) + string ownerName; + if (_playerCache.TryGetValue(retainer.OwnerLocalContentId, out CachedPlayer? cachedPlayer)) + ownerName = cachedPlayer.Name; + else ownerName = retainer.OwnerLocalContentId.ToString(CultureInfo.InvariantCulture); _logger.LogDebug(" Retainer {RetainerName} belongs to {OwnerName}", retainer.Name, ownerName); @@ -145,45 +161,94 @@ internal sealed class PersistenceContext } } - public void HandleContentIdMapping(ContentIdToName mapping) - => HandleContentIdMapping(new List { mapping }); - - public void HandleContentIdMapping(IReadOnlyList mappings) + private void HandleContentIdMappingFallback(PlayerMapping mapping) { try { - var updates = mappings.DistinctBy(x => x.ContentId) - .Where(mapping => mapping.ContentId != 0 && !string.IsNullOrEmpty(mapping.PlayerName)) - .Where(mapping => - { - if (_playerNameCache.TryGetValue(mapping.ContentId, out string? existingName)) - return mapping.PlayerName != existingName; + if (mapping.ContentId == 0 || string.IsNullOrEmpty(mapping.PlayerName)) + return; - return true; - }) - .Select(mapping => - new Player + if (_playerCache.TryGetValue(mapping.ContentId, out CachedPlayer? cachedPlayer)) + { + if (mapping.PlayerName == cachedPlayer.Name && mapping.AccountId == cachedPlayer.AccountId) + return; + } + + using (var scope = _serviceProvider.CreateScope()) + { + using var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbPlayer = dbContext.Players.Find(mapping.ContentId); + if (dbPlayer == null) + dbContext.Players.Add(new Player { LocalContentId = mapping.ContentId, Name = mapping.PlayerName, - }) - .ToList(); + AccountId = mapping.AccountId, + }); + else + { + dbPlayer.Name = mapping.PlayerName; + dbPlayer.AccountId ??= mapping.AccountId; + dbContext.Entry(dbPlayer).State = EntityState.Modified; + } - if (updates.Count == 0) - return; + int changeCount = dbContext.SaveChanges(); + if (changeCount > 0) + { + _logger.LogDebug("Saved fallback player mappings for {ContentId} / {Name} / {AccountId}", + mapping.ContentId, mapping.PlayerName, mapping.AccountId); + } + _playerCache[mapping.ContentId] = new CachedPlayer + { + AccountId = mapping.AccountId, + Name = mapping.PlayerName, + }; + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not persist singular mapping for {ContentId} / {Name} / {AccountId}", + mapping.ContentId, mapping.PlayerName, mapping.AccountId); + } + } + + public void HandleContentIdMapping(IReadOnlyList mappings) + { + var updates = mappings.DistinctBy(x => x.ContentId) + .Where(mapping => mapping.ContentId != 0 && !string.IsNullOrEmpty(mapping.PlayerName)) + .Where(mapping => + { + if (_playerCache.TryGetValue(mapping.ContentId, out CachedPlayer? cachedPlayer)) + return mapping.PlayerName != cachedPlayer.Name || mapping.AccountId != cachedPlayer.AccountId; + + return true; + }) + .ToList(); + + if (updates.Count == 0) + return; + + try + { using (var scope = _serviceProvider.CreateScope()) { using var dbContext = scope.ServiceProvider.GetRequiredService(); foreach (var update in updates) { - var dbPlayer = dbContext.Players.Find(update.LocalContentId); + var dbPlayer = dbContext.Players.Find(update.ContentId); if (dbPlayer == null) - dbContext.Players.Add(update); + dbContext.Players.Add(new Player + { + LocalContentId = update.ContentId, + Name = update.PlayerName, + AccountId = update.AccountId, + }); else { - dbPlayer.Name = update.Name; - dbContext.Players.Update(dbPlayer); + dbPlayer.Name = update.PlayerName; + dbPlayer.AccountId ??= update.AccountId; + dbContext.Entry(dbPlayer).State = EntityState.Modified; } } @@ -192,16 +257,33 @@ internal sealed class PersistenceContext { _logger.LogDebug("Saved {Count} player mappings", changeCount); foreach (var update in updates) - _logger.LogTrace(" {ContentId} = {Name}", update.LocalContentId, update.Name); + _logger.LogTrace(" {ContentId} = {Name} ({AccountId})", update.ContentId, update.PlayerName, + update.AccountId); } } foreach (var player in updates) - _playerNameCache[player.LocalContentId] = player.Name ?? string.Empty; + { + _playerCache[player.ContentId] = new CachedPlayer + { + AccountId = player.AccountId, + Name = player.PlayerName, + }; + } } catch (Exception e) { - _logger.LogError(e, "Could not persist multiple mappings"); + _logger.LogWarning(e, "Could not persist multiple mappings, attempting non-batch update"); + foreach (var update in updates) + { + HandleContentIdMappingFallback(update); + } } } + + public sealed class CachedPlayer + { + public required ulong? AccountId { get; init; } + public required string Name { get; init; } + } } diff --git a/RetainerTrack/Handlers/PlayerMapping.cs b/RetainerTrack/Handlers/PlayerMapping.cs new file mode 100644 index 0000000..7788906 --- /dev/null +++ b/RetainerTrack/Handlers/PlayerMapping.cs @@ -0,0 +1,8 @@ +namespace RetainerTrack.Handlers; + +internal sealed class PlayerMapping +{ + public required ulong? AccountId { get; init; } + public required ulong ContentId { get; init; } + public required string PlayerName { get; init; } = string.Empty; +} diff --git a/RetainerTrack/RetainerTrack.csproj b/RetainerTrack/RetainerTrack.csproj index ed2d724..9c4e033 100644 --- a/RetainerTrack/RetainerTrack.csproj +++ b/RetainerTrack/RetainerTrack.csproj @@ -1,72 +1,20 @@ - - + - net8.0-windows 4.0 - 12.0 - enable win-x64 none - true - false - false - dist - true - true - portable - $(SolutionDir)=X:\ + dist - - $(appdata)\XIVLauncher\addon\Hooks\dev\ - - - - $(DALAMUD_HOME)/ - + + - all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - $(DalamudLibPath)Dalamud.dll - false - - - $(DalamudLibPath)ImGui.NET.dll - false - - - $(DalamudLibPath)ImGuiScene.dll - false - - - $(DalamudLibPath)Lumina.dll - false - - - $(DalamudLibPath)Lumina.Excel.dll - false - - - $(DalamudLibPath)Newtonsoft.Json.dll - false - - - $(DalamudLibPath)FFXIVClientStructs.dll - false - - - - - - diff --git a/RetainerTrack/RetainerTrackPlugin.cs b/RetainerTrack/RetainerTrackPlugin.cs index 76d6e9e..052d1fe 100644 --- a/RetainerTrack/RetainerTrackPlugin.cs +++ b/RetainerTrack/RetainerTrackPlugin.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Logging; using RetainerTrack.Commands; using RetainerTrack.Database; using RetainerTrack.Database.Compiled; -using RetainerTrack.Database.Migrations; using RetainerTrack.Handlers; namespace RetainerTrack; @@ -23,7 +22,7 @@ internal sealed class RetainerTrackPlugin : IDalamudPlugin private readonly ServiceProvider? _serviceProvider; public RetainerTrackPlugin( - DalamudPluginInterface pluginInterface, + IDalamudPluginInterface pluginInterface, IFramework framework, IClientState clientState, IGameGui gameGui, @@ -32,6 +31,8 @@ internal sealed class RetainerTrackPlugin : IDalamudPlugin IAddonLifecycle addonLifecycle, ICommandManager commandManager, IDataManager dataManager, + IObjectTable objectTable, + IMarketBoard marketBoard, IPluginLog pluginLog) { ServiceCollection serviceCollection = new(); @@ -49,11 +50,13 @@ internal sealed class RetainerTrackPlugin : IDalamudPlugin serviceCollection.AddSingleton(addonLifecycle); serviceCollection.AddSingleton(commandManager); serviceCollection.AddSingleton(dataManager); + serviceCollection.AddSingleton(objectTable); + serviceCollection.AddSingleton(marketBoard); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -68,9 +71,9 @@ internal sealed class RetainerTrackPlugin : IDalamudPlugin RunMigrations(_serviceProvider); - _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); } diff --git a/RetainerTrack/packages.lock.json b/RetainerTrack/packages.lock.json index 28b2620..70de249 100644 --- a/RetainerTrack/packages.lock.json +++ b/RetainerTrack/packages.lock.json @@ -13,9 +13,21 @@ }, "DalamudPackager": { "type": "Direct", - "requested": "[2.1.12, )", - "resolved": "2.1.12", - "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" + "requested": "[2.1.13, )", + "resolved": "2.1.13", + "contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ==" + }, + "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" + } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", @@ -27,6 +39,21 @@ "SQLitePCLRaw.bundle_e_sqlite3": "2.1.6" } }, + "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" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", "resolved": "8.0.5", @@ -157,6 +184,47 @@ "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, + "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" + } + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.6",