using Roboto.Memory; using Serilog; namespace Roboto.Memory.Objects; /// /// Reads quest flags from ServerData → PlayerServerData → QuestFlags. /// RemoteObject wrapping the previous QuestReader logic. /// Update(serverDataPtr) to read quests. /// public sealed class QuestFlags : RemoteObject { private readonly MsvcStringReader _strings; private readonly QuestNameLookup? _nameLookup; private readonly Dictionary _nameCache = new(); private nint _lastPsd; public List? 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? 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(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? 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(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; } }