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;
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user