Remove opcode dependency, add contentId matching via social lists
This commit is contained in:
parent
f453a5b812
commit
8285ee8634
@ -1,23 +1,8 @@
|
|||||||
using System.IO;
|
namespace RetainerTrack.Handlers
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace RetainerTrack.Handlers
|
|
||||||
{
|
{
|
||||||
internal sealed class ContentIdToName
|
internal sealed class ContentIdToName
|
||||||
{
|
{
|
||||||
public ulong ContentId { get; init; }
|
public ulong ContentId { get; init; }
|
||||||
public string PlayerName { get; init; } = string.Empty;
|
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
161
RetainerTrack/Handlers/GameHooks.cs
Normal file
161
RetainerTrack/Handlers/GameHooks.cs
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,9 +17,7 @@ namespace RetainerTrack.Handlers
|
|||||||
private readonly ClientState _clientState;
|
private readonly ClientState _clientState;
|
||||||
private readonly PersistenceContext _persistenceContext;
|
private readonly PersistenceContext _persistenceContext;
|
||||||
|
|
||||||
private readonly ushort? _contentIdMappingOpCode;
|
public NetworkHandler(
|
||||||
|
|
||||||
public unsafe NetworkHandler(
|
|
||||||
ILogger<NetworkHandler> logger,
|
ILogger<NetworkHandler> logger,
|
||||||
GameNetwork gameNetwork,
|
GameNetwork gameNetwork,
|
||||||
DataManager dataManager,
|
DataManager dataManager,
|
||||||
@ -32,14 +30,6 @@ namespace RetainerTrack.Handlers
|
|||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_persistenceContext = persistenceContext;
|
_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;
|
_gameNetwork.NetworkMessage += NetworkMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,13 +56,6 @@ namespace RetainerTrack.Handlers
|
|||||||
var listings = MarketBoardCurrentOfferings.Read(dataPtr);
|
var listings = MarketBoardCurrentOfferings.Read(dataPtr);
|
||||||
Task.Run(() => _persistenceContext.HandleMarketBoardPage(listings, worldId));
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,12 +32,13 @@ namespace RetainerTrack.Handlers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_lastUpdate = now;
|
_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();
|
var groupManager = GroupManager.Instance();
|
||||||
foreach (var partyMember in groupManager->PartyMembersSpan)
|
if (groupManager->AllianceFlags == 0x0)
|
||||||
HandlePartyMember(partyMember, mappings);
|
return;
|
||||||
|
|
||||||
|
List<ContentIdToName> mappings = new();
|
||||||
foreach (var allianceMember in groupManager->AllianceMembersSpan)
|
foreach (var allianceMember in groupManager->AllianceMembersSpan)
|
||||||
HandlePartyMember(allianceMember, mappings);
|
HandlePartyMember(allianceMember, mappings);
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ namespace RetainerTrack.Handlers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
string name = MemoryHelper.ReadStringNullTerminated((nint)partyMember.Name);
|
string name = MemoryHelper.ReadStringNullTerminated((nint)partyMember.Name);
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
return;
|
||||||
|
|
||||||
contentIdToNames.Add(new ContentIdToName
|
contentIdToNames.Add(new ContentIdToName
|
||||||
{
|
{
|
||||||
ContentId = (ulong)partyMember.ContentID,
|
ContentId = (ulong)partyMember.ContentID,
|
||||||
|
@ -101,6 +101,13 @@ namespace RetainerTrack.Handlers
|
|||||||
{
|
{
|
||||||
var updates = mappings
|
var updates = mappings
|
||||||
.Where(mapping => mapping.ContentId != 0 && !string.IsNullOrEmpty(mapping.PlayerName))
|
.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 =>
|
.Select(mapping =>
|
||||||
new Player
|
new Player
|
||||||
{
|
{
|
||||||
|
@ -24,10 +24,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0"/>
|
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0" />
|
||||||
<PackageReference Include="DalamudPackager" Version="2.1.10"/>
|
<PackageReference Include="DalamudPackager" Version="2.1.10" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.15"/>
|
<PackageReference Include="LiteDB" Version="5.0.15" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -53,6 +53,7 @@ namespace RetainerTrack
|
|||||||
serviceCollection.AddSingleton<NetworkHandler>();
|
serviceCollection.AddSingleton<NetworkHandler>();
|
||||||
serviceCollection.AddSingleton<PartyHandler>();
|
serviceCollection.AddSingleton<PartyHandler>();
|
||||||
serviceCollection.AddSingleton<MarketBoardUiHandler>();
|
serviceCollection.AddSingleton<MarketBoardUiHandler>();
|
||||||
|
serviceCollection.AddSingleton<GameHooks>();
|
||||||
|
|
||||||
_serviceProvider = serviceCollection.BuildServiceProvider();
|
_serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ namespace RetainerTrack
|
|||||||
_serviceProvider.GetRequiredService<PartyHandler>();
|
_serviceProvider.GetRequiredService<PartyHandler>();
|
||||||
_serviceProvider.GetRequiredService<NetworkHandler>();
|
_serviceProvider.GetRequiredService<NetworkHandler>();
|
||||||
_serviceProvider.GetRequiredService<MarketBoardUiHandler>();
|
_serviceProvider.GetRequiredService<MarketBoardUiHandler>();
|
||||||
|
_serviceProvider.GetRequiredService<GameHooks>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
Loading…
Reference in New Issue
Block a user