Remove opcode dependency, add contentId matching via social lists

This commit is contained in:
Liza 2023-03-02 00:00:18 +01:00
parent f453a5b812
commit 8285ee8634
Signed by: liza
GPG Key ID: 7199F8D727D55F67
7 changed files with 183 additions and 41 deletions

View File

@ -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)
};
}
}
}

View File

@ -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<GameHooks> _logger;
private readonly PersistenceContext _persistenceContext;
/// <summary>
/// Processes the content id to character name packet, seen e.g. when you hover an item to retrieve the
/// crafter's signature.
/// </summary>
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<CharacterNameResultDelegate> 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<SocialListResultDelegate> SocialListResultHook { get; init; } = null!;
#pragma warning restore CS0649
public GameHooks(ILogger<GameHooks> 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<SocialListResultPage>(dataPtr);
List<ContentIdToName> 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();
}
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 0x380)]
internal struct SocialListResultPage
{
[FieldOffset(0x10)] private fixed byte Players[10 * 0x58];
public Span<SocialListPlayer> PlayerSpan => new(Unsafe.AsPointer(ref Players[0]), 10);
}
[StructLayout(LayoutKind.Explicit, Size = 0x58)]
internal struct SocialListPlayer
{
/// <summary>
/// If this is set, it means there is a player present in this slot (even if no name can be retrieved),
/// 0 if empty.
/// </summary>
[FieldOffset(0x00)] public readonly ulong ContentId;
/// <summary>
/// This *can* be empty, e.g. if you're querying your friend list, the names are ONLY set for characters on the same world.
/// </summary>
[FieldOffset(0x31)] public fixed byte CharacterName[32];
}
}
}

View File

@ -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<NetworkHandler> 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 : "<unknown>");
Task.Run(() => _persistenceContext.HandleContentIdMapping(mapping));
}
}
}
}

View File

@ -32,12 +32,13 @@ namespace RetainerTrack.Handlers
return;
_lastUpdate = now;
List<ContentIdToName> 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<ContentIdToName> 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,

View File

@ -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
{

View File

@ -24,10 +24,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0"/>
<PackageReference Include="DalamudPackager" Version="2.1.10"/>
<PackageReference Include="LiteDB" Version="5.0.15"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0"/>
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0" />
<PackageReference Include="DalamudPackager" Version="2.1.10" />
<PackageReference Include="LiteDB" Version="5.0.15" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -53,6 +53,7 @@ namespace RetainerTrack
serviceCollection.AddSingleton<NetworkHandler>();
serviceCollection.AddSingleton<PartyHandler>();
serviceCollection.AddSingleton<MarketBoardUiHandler>();
serviceCollection.AddSingleton<GameHooks>();
_serviceProvider = serviceCollection.BuildServiceProvider();
@ -65,6 +66,7 @@ namespace RetainerTrack
_serviceProvider.GetRequiredService<PartyHandler>();
_serviceProvider.GetRequiredService<NetworkHandler>();
_serviceProvider.GetRequiredService<MarketBoardUiHandler>();
_serviceProvider.GetRequiredService<GameHooks>();
}
public void Dispose()