poe2-bot/src/Roboto.Memory/Objects/QuestFlags.cs
2026-03-04 16:49:30 -05:00

266 lines
9.3 KiB
C#

using Roboto.Memory;
using Serilog;
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads quest flags from ServerData → PlayerServerData → QuestFlags.
/// RemoteObject wrapping the previous QuestReader logic.
/// Update(serverDataPtr) to read quests.
/// </summary>
public sealed class QuestFlags : RemoteObject
{
private readonly MsvcStringReader _strings;
private readonly QuestNameLookup? _nameLookup;
private readonly Dictionary<nint, string?> _nameCache = new();
private nint _lastPsd;
public List<QuestSnapshot>? Quests { get; private set; }
public QuestFlags(MemoryContext ctx, MsvcStringReader strings, QuestNameLookup? nameLookup = null)
: base(ctx)
{
_strings = strings;
_nameLookup = nameLookup;
}
protected override bool ReadData()
{
var offsets = Ctx.Offsets;
if (offsets.QuestFlagEntrySize <= 0) { Quests = null; return true; }
var mem = Ctx.Memory;
var psdVecBegin = mem.ReadPointer(Address + offsets.PlayerServerDataOffset);
if (psdVecBegin == 0) { Quests = null; return true; }
var playerServerData = mem.ReadPointer(psdVecBegin);
if (playerServerData == 0) { Quests = null; return true; }
if (playerServerData != _lastPsd)
{
_nameCache.Clear();
_lastPsd = playerServerData;
}
var questFlagsAddr = playerServerData + offsets.QuestFlagsOffset;
if (offsets.QuestFlagsContainerType == "int_vector")
Quests = ReadIntVectorQuests(questFlagsAddr, offsets);
else if (offsets.QuestFlagsContainerType == "vector")
Quests = ReadStructVectorQuests(questFlagsAddr, offsets);
else
Quests = null;
return true;
}
protected override void Clear()
{
Quests = null;
_nameCache.Clear();
_lastPsd = 0;
}
private List<QuestSnapshot>? ReadIntVectorQuests(nint questFlagsAddr, GameOffsets offsets)
{
var mem = Ctx.Memory;
var vecBegin = mem.ReadPointer(questFlagsAddr);
var vecEnd = mem.ReadPointer(questFlagsAddr + 8);
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
var totalBytes = (int)(vecEnd - vecBegin);
var entryCount = totalBytes / 4;
if (entryCount <= 0 || entryCount > offsets.QuestFlagsMaxEntries) return null;
var vecData = mem.ReadBytes(vecBegin, totalBytes);
if (vecData is null) return null;
byte[]? compData = null;
var compEntryCount = 0;
if (offsets.QuestCompanionOffset > 0 && offsets.QuestCompanionEntrySize > 0)
{
var compBegin = mem.ReadPointer(questFlagsAddr + offsets.QuestCompanionOffset);
var compEnd = mem.ReadPointer(questFlagsAddr + offsets.QuestCompanionOffset + 8);
if (compBegin != 0 && compEnd > compBegin)
{
var compBytes = (int)(compEnd - compBegin);
compEntryCount = compBytes / offsets.QuestCompanionEntrySize;
if (compEntryCount > 0 && compBytes < 0x100000)
compData = mem.ReadBytes(compBegin, compBytes);
}
}
var datTableBase = FindDatTableBase(offsets);
var result = new List<QuestSnapshot>(entryCount);
for (var i = 0; i < entryCount; i++)
{
var idx = BitConverter.ToInt32(vecData, i * 4);
string? questName = null;
string? internalId = null;
byte stateId = 0;
bool isTracked = false;
nint questObjPtr = 0;
if (compData is not null && i < compEntryCount)
{
var compOff = i * offsets.QuestCompanionEntrySize;
if (offsets.QuestCompanionTrackedOffset > 0 &&
compOff + offsets.QuestCompanionTrackedOffset + 4 <= compData.Length)
{
var trackedVal = BitConverter.ToUInt32(compData, compOff + offsets.QuestCompanionTrackedOffset);
isTracked = trackedVal == offsets.QuestTrackedMarker;
}
if (compOff + offsets.QuestCompanionObjPtrOffset + 8 <= compData.Length)
{
questObjPtr = (nint)BitConverter.ToInt64(compData, compOff + offsets.QuestCompanionObjPtrOffset);
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF
&& offsets.QuestObjEncounterStateOffset > 0)
{
var stateByte = mem.ReadBytes(questObjPtr + offsets.QuestObjEncounterStateOffset, 1);
if (stateByte is { Length: 1 })
stateId = stateByte[0];
}
}
}
if (datTableBase != 0 && offsets.QuestDatRowSize > 0)
{
var rowAddr = datTableBase + idx * offsets.QuestDatRowSize;
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
}
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
{
questName = entry?.Name;
internalId = entry?.InternalId;
}
result.Add(new QuestSnapshot
{
QuestStateIndex = idx,
QuestDatPtr = questObjPtr,
QuestName = questName,
InternalId = internalId,
StateId = stateId,
IsTracked = isTracked,
});
}
return result;
}
private nint FindDatTableBase(GameOffsets offsets)
{
if (offsets.QuestDatRowSize <= 0) return 0;
return 0;
}
private string? ResolveDatString(nint fieldAddr)
{
if (_nameCache.TryGetValue(fieldAddr, out var cached))
return cached;
var mem = Ctx.Memory;
var strPtr = mem.ReadPointer(fieldAddr);
string? result = null;
if (strPtr != 0 && ((ulong)strPtr >> 32) is > 0 and < 0x7FFF)
result = _strings.ReadNullTermWString(strPtr);
_nameCache[fieldAddr] = result;
return result;
}
private List<QuestSnapshot>? ReadStructVectorQuests(nint questFlagsAddr, GameOffsets offsets)
{
var mem = Ctx.Memory;
var vecBegin = mem.ReadPointer(questFlagsAddr);
var vecEnd = mem.ReadPointer(questFlagsAddr + 8);
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
var totalBytes = (int)(vecEnd - vecBegin);
var entrySize = offsets.QuestFlagEntrySize;
var entryCount = totalBytes / entrySize;
if (entryCount <= 0 || entryCount > offsets.QuestFlagsMaxEntries) return null;
var vecData = mem.ReadBytes(vecBegin, totalBytes);
if (vecData is null) return null;
var result = new List<QuestSnapshot>(entryCount);
for (var i = 0; i < entryCount; i++)
{
var entryOffset = i * entrySize;
nint questDatPtr = 0;
if (entryOffset + offsets.QuestEntryQuestPtrOffset + 8 <= vecData.Length)
questDatPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryQuestPtrOffset);
byte stateId = 0;
if (entryOffset + offsets.QuestEntryStateIdOffset < vecData.Length)
stateId = vecData[entryOffset + offsets.QuestEntryStateIdOffset];
var questName = ResolveQuestName(questDatPtr);
string? stateText = null;
if (offsets.QuestEntryStateTextOffset > 0 &&
entryOffset + offsets.QuestEntryStateTextOffset + 8 <= vecData.Length)
{
var stateTextPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryStateTextOffset);
if (stateTextPtr != 0 && ((ulong)stateTextPtr >> 32) is > 0 and < 0x7FFF)
stateText = _strings.ReadNullTermWString(stateTextPtr);
}
string? progressText = null;
if (offsets.QuestEntryProgressTextOffset > 0 &&
entryOffset + offsets.QuestEntryProgressTextOffset + 8 <= vecData.Length)
{
var progressTextPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryProgressTextOffset);
if (progressTextPtr != 0 && ((ulong)progressTextPtr >> 32) is > 0 and < 0x7FFF)
progressText = _strings.ReadNullTermWString(progressTextPtr);
}
result.Add(new QuestSnapshot
{
QuestDatPtr = questDatPtr,
QuestName = questName,
StateId = stateId,
StateText = stateText,
ProgressText = progressText,
});
}
return result;
}
private string? ResolveQuestName(nint questDatPtr)
{
if (questDatPtr == 0) return null;
if (_nameCache.TryGetValue(questDatPtr, out var cached))
return cached;
var mem = Ctx.Memory;
string? name = null;
var high = (ulong)questDatPtr >> 32;
if (high is > 0 and < 0x7FFF)
{
var namePtr = mem.ReadPointer(questDatPtr);
if (namePtr != 0)
name = _strings.ReadNullTermWString(namePtr);
}
_nameCache[questDatPtr] = name;
return name;
}
}