From 8285ee8634c29b47bd476449e77f54598cc603cb Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 2 Mar 2023 00:00:18 +0100 Subject: [PATCH] Remove opcode dependency, add contentId matching via social lists --- RetainerTrack/Handlers/ContentIdToName.cs | 17 +- RetainerTrack/Handlers/GameHooks.cs | 161 +++++++++++++++++++ RetainerTrack/Handlers/NetworkHandler.cs | 19 +-- RetainerTrack/Handlers/PartyHandler.cs | 10 +- RetainerTrack/Handlers/PersistenceContext.cs | 7 + RetainerTrack/RetainerTrack.csproj | 8 +- RetainerTrack/RetainerTrackPlugin.cs | 2 + 7 files changed, 183 insertions(+), 41 deletions(-) create mode 100644 RetainerTrack/Handlers/GameHooks.cs diff --git a/RetainerTrack/Handlers/ContentIdToName.cs b/RetainerTrack/Handlers/ContentIdToName.cs index 048b1d7..5221467 100644 --- a/RetainerTrack/Handlers/ContentIdToName.cs +++ b/RetainerTrack/Handlers/ContentIdToName.cs @@ -1,23 +1,8 @@ -using System.IO; -using System.Text; - -namespace RetainerTrack.Handlers +namespace RetainerTrack.Handlers { internal sealed class ContentIdToName { public ulong ContentId { get; init; } public string PlayerName { get; init; } = string.Empty; - - public static unsafe ContentIdToName ReadFromNetworkPacket(nint dataPtr) - { - using UnmanagedMemoryStream input = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 40); - using BinaryReader binaryReader = new BinaryReader(input); - - return new ContentIdToName - { - ContentId = binaryReader.ReadUInt64(), - PlayerName = Encoding.UTF8.GetString(binaryReader.ReadBytes(32)).TrimEnd(char.MinValue) - }; - } } } diff --git a/RetainerTrack/Handlers/GameHooks.cs b/RetainerTrack/Handlers/GameHooks.cs new file mode 100644 index 0000000..6a93a74 --- /dev/null +++ b/RetainerTrack/Handlers/GameHooks.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Hooking; +using Dalamud.Memory; +using Dalamud.Utility.Signatures; +using Microsoft.Extensions.Logging; + +namespace RetainerTrack.Handlers +{ + internal sealed unsafe class GameHooks : IDisposable + { + private readonly ILogger _logger; + private readonly PersistenceContext _persistenceContext; + + /// + /// Processes the content id to character name packet, seen e.g. when you hover an item to retrieve the + /// crafter's signature. + /// + private delegate int CharacterNameResultDelegate(nint a1, ulong contentId, char* playerName); + + private delegate nint SocialListResultDelegate(nint a1, nint dataPtr); + +#pragma warning disable CS0649 + [Signature("40 53 48 83 EC 20 48 8B D9 33 C9 45 33 C9", DetourName = nameof(ProcessCharacterNameResult))] + private Hook CharacterNameResultHook { get; init; } = null!; + + // Signature adapted from https://github.com/LittleNightmare/UsedName + [Signature("48 89 5C 24 10 56 48 83 EC 20 48 ?? ?? ?? ?? ?? ?? 48 8B F2 E8 ?? ?? ?? ?? 48 8B D8", + DetourName = nameof(ProcessSocialListResult))] + private Hook SocialListResultHook { get; init; } = null!; + +#pragma warning restore CS0649 + + public GameHooks(ILogger logger, PersistenceContext persistenceContext) + { + _logger = logger; + _persistenceContext = persistenceContext; + + _logger.LogDebug("Initializing game hooks"); + SignatureHelper.Initialise(this); + CharacterNameResultHook.Enable(); + SocialListResultHook.Enable(); + + _logger.LogDebug("Game hooks initialized"); + } + + private int ProcessCharacterNameResult(nint a1, ulong contentId, char* playerName) + { + try + { + var mapping = new ContentIdToName + { + ContentId = contentId, + PlayerName = MemoryHelper.ReadString(new nint(playerName), Encoding.ASCII, 32), + }; + + if (!string.IsNullOrEmpty(mapping.PlayerName)) + { + _logger.LogTrace("Content id {ContentId} belongs to '{Name}'", mapping.ContentId, + mapping.PlayerName); + Task.Run(() => _persistenceContext.HandleContentIdMapping(mapping)); + } + else + { + _logger.LogDebug("Content id {ContentId} didn't resolve to a player name, ignoring", + mapping.ContentId); + } + } + catch (Exception e) + { + _logger.LogError(e, "Could not process character name result"); + } + + return CharacterNameResultHook.Original(a1, contentId, playerName); + } + + private nint ProcessSocialListResult(nint a1, nint dataPtr) + { + try + { + var result = Marshal.PtrToStructure(dataPtr); + List mappings = new(); + foreach (SocialListPlayer player in result.PlayerSpan) + { + var mapping = new ContentIdToName + { + ContentId = player.ContentId, + PlayerName = MemoryHelper.ReadString(new nint(player.CharacterName), Encoding.ASCII, 32), + }; + + if (!string.IsNullOrEmpty(mapping.PlayerName)) + { + _logger.LogTrace("Content id {ContentId} belongs to '{Name}'", mapping.ContentId, + mapping.PlayerName); + mappings.Add(mapping); + } + else + { + _logger.LogDebug("Content id {ContentId} didn't resolve to a player name, ignoring", + mapping.ContentId); + } + } + + if (mappings.Count > 0) + Task.Run(() => _persistenceContext.HandleContentIdMapping(mappings)); + } + catch (Exception e) + { + _logger.LogError(e, "Could not process social list result"); + } + + return SocialListResultHook.Original(a1, dataPtr); + } + + public void Dispose() + { + CharacterNameResultHook.Dispose(); + SocialListResultHook.Dispose(); + } + + /// + /// There are some caveats here, the social list includes a LOT of things with different types + /// (we don't care for the result type in this plugin), see sapphire for which field is the type. + /// + /// 1 = party + /// 2 = friend list + /// 3 = link shell + /// 4 = player search + /// 5 = fc short list (first tab, with company board + actions + online members) + /// 6 = fc long list (members tab) + /// + /// Both 1 and 2 are sent to you on login, unprompted. + /// + [StructLayout(LayoutKind.Explicit, Size = 0x380)] + internal struct SocialListResultPage + { + [FieldOffset(0x10)] private fixed byte Players[10 * 0x58]; + + public Span PlayerSpan => new(Unsafe.AsPointer(ref Players[0]), 10); + } + + [StructLayout(LayoutKind.Explicit, Size = 0x58)] + internal struct SocialListPlayer + { + /// + /// If this is set, it means there is a player present in this slot (even if no name can be retrieved), + /// 0 if empty. + /// + [FieldOffset(0x00)] public readonly ulong ContentId; + + /// + /// 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(0x31)] public fixed byte CharacterName[32]; + } + } +} diff --git a/RetainerTrack/Handlers/NetworkHandler.cs b/RetainerTrack/Handlers/NetworkHandler.cs index 8b5968d..326971c 100644 --- a/RetainerTrack/Handlers/NetworkHandler.cs +++ b/RetainerTrack/Handlers/NetworkHandler.cs @@ -17,9 +17,7 @@ namespace RetainerTrack.Handlers private readonly ClientState _clientState; private readonly PersistenceContext _persistenceContext; - private readonly ushort? _contentIdMappingOpCode; - - public unsafe NetworkHandler( + public NetworkHandler( ILogger logger, GameNetwork gameNetwork, DataManager dataManager, @@ -32,14 +30,6 @@ namespace RetainerTrack.Handlers _clientState = clientState; _persistenceContext = persistenceContext; - if (Framework.Instance()->GameVersion.Base == "2023.02.03.0000.0000") - _contentIdMappingOpCode = 0x01C4; - else - { - _logger.LogWarning("Not tracking content id mappings, unsupported game version {Version}", - Framework.Instance()->GameVersion.Base); - } - _gameNetwork.NetworkMessage += NetworkMessage; } @@ -66,13 +56,6 @@ namespace RetainerTrack.Handlers var listings = MarketBoardCurrentOfferings.Read(dataPtr); Task.Run(() => _persistenceContext.HandleMarketBoardPage(listings, worldId)); } - else if (opcode == _contentIdMappingOpCode) - { - var mapping = ContentIdToName.ReadFromNetworkPacket(dataPtr); - _logger.LogTrace("Content id {ContentId} belongs to player '{Name}'", mapping.ContentId, - !string.IsNullOrEmpty(mapping.PlayerName) ? mapping.PlayerName : ""); - Task.Run(() => _persistenceContext.HandleContentIdMapping(mapping)); - } } } } diff --git a/RetainerTrack/Handlers/PartyHandler.cs b/RetainerTrack/Handlers/PartyHandler.cs index 9fecc70..db1eaed 100644 --- a/RetainerTrack/Handlers/PartyHandler.cs +++ b/RetainerTrack/Handlers/PartyHandler.cs @@ -32,12 +32,13 @@ namespace RetainerTrack.Handlers return; _lastUpdate = now; - List mappings = new(); + // skip if we're not in an alliance, party members are handled via social list updates var groupManager = GroupManager.Instance(); - foreach (var partyMember in groupManager->PartyMembersSpan) - HandlePartyMember(partyMember, mappings); + if (groupManager->AllianceFlags == 0x0) + return; + List mappings = new(); foreach (var allianceMember in groupManager->AllianceMembersSpan) HandlePartyMember(allianceMember, mappings); @@ -51,6 +52,9 @@ namespace RetainerTrack.Handlers return; string name = MemoryHelper.ReadStringNullTerminated((nint)partyMember.Name); + if (string.IsNullOrEmpty(name)) + return; + contentIdToNames.Add(new ContentIdToName { ContentId = (ulong)partyMember.ContentID, diff --git a/RetainerTrack/Handlers/PersistenceContext.cs b/RetainerTrack/Handlers/PersistenceContext.cs index 70f5f67..0b8c277 100644 --- a/RetainerTrack/Handlers/PersistenceContext.cs +++ b/RetainerTrack/Handlers/PersistenceContext.cs @@ -101,6 +101,13 @@ namespace RetainerTrack.Handlers { var updates = mappings .Where(mapping => mapping.ContentId != 0 && !string.IsNullOrEmpty(mapping.PlayerName)) + .Where(mapping => + { + if (_playerNameCache.TryGetValue(mapping.ContentId, out string? existingName)) + return mapping.PlayerName != existingName; + + return true; + }) .Select(mapping => new Player { diff --git a/RetainerTrack/RetainerTrack.csproj b/RetainerTrack/RetainerTrack.csproj index 7808664..580293c 100644 --- a/RetainerTrack/RetainerTrack.csproj +++ b/RetainerTrack/RetainerTrack.csproj @@ -24,10 +24,10 @@ - - - - + + + + diff --git a/RetainerTrack/RetainerTrackPlugin.cs b/RetainerTrack/RetainerTrackPlugin.cs index e91e534..2d80e0b 100644 --- a/RetainerTrack/RetainerTrackPlugin.cs +++ b/RetainerTrack/RetainerTrackPlugin.cs @@ -53,6 +53,7 @@ namespace RetainerTrack serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); _serviceProvider = serviceCollection.BuildServiceProvider(); @@ -65,6 +66,7 @@ namespace RetainerTrack _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); } public void Dispose()