stuff
This commit is contained in:
parent
a8341e8232
commit
a8c43ba7e2
43 changed files with 2618 additions and 48 deletions
|
|
@ -291,6 +291,19 @@ public sealed class ComponentReader
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the player character name from the Player component.
|
||||
/// </summary>
|
||||
public string? ReadPlayerName(nint localPlayerEntity)
|
||||
{
|
||||
if (localPlayerEntity == 0) return null;
|
||||
|
||||
var playerComp = GetComponentAddress(localPlayerEntity, "Player");
|
||||
if (playerComp == 0) return null;
|
||||
|
||||
return _strings.ReadMsvcWString(playerComp + 0x1B0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves EntityDetails pointer for an entity, handling ECS inner entity wrapper.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ public class Entity
|
|||
|
||||
// AreaTransition destination (raw area ID, e.g. "G1_4")
|
||||
public string? TransitionName { get; internal set; }
|
||||
public int TransitionState { get; internal set; } = -1;
|
||||
|
||||
// Action state (from Actor component)
|
||||
public short ActionId { get; internal set; }
|
||||
|
|
|
|||
|
|
@ -80,6 +80,17 @@ public sealed class EntityReader
|
|||
|
||||
var (compFirst, compCount) = _components.FindComponentList(entityPtr);
|
||||
|
||||
// Read Targetable for any entity that has it
|
||||
if (lookup.TryGetValue("Targetable", out var targetIdx) && targetIdx >= 0 && targetIdx < compCount)
|
||||
{
|
||||
var targetComp = mem.ReadPointer(compFirst + targetIdx * 8);
|
||||
if (targetComp != 0)
|
||||
{
|
||||
var targetable = mem.Read<Targetable>(targetComp);
|
||||
entity.IsTargetable = targetable.IsTargetable != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Read HP/Actor/Mods for monsters
|
||||
if (entity.Components.Contains("Monster"))
|
||||
{
|
||||
|
|
@ -119,7 +130,7 @@ public sealed class EntityReader
|
|||
}
|
||||
}
|
||||
|
||||
// Read AreaTransition destination name
|
||||
// Read AreaTransition destination + Transitionable state
|
||||
if (entity.Components.Contains("AreaTransition") &&
|
||||
lookup.TryGetValue("AreaTransition", out var atIdx) && atIdx >= 0 && atIdx < compCount)
|
||||
{
|
||||
|
|
@ -127,6 +138,17 @@ public sealed class EntityReader
|
|||
if (atComp != 0)
|
||||
entity.TransitionName = ReadAreaTransitionName(atComp);
|
||||
}
|
||||
|
||||
if (entity.Components.Contains("Transitionable") &&
|
||||
lookup.TryGetValue("Transitionable", out var trIdx) && trIdx >= 0 && trIdx < compCount)
|
||||
{
|
||||
var trComp = mem.ReadPointer(compFirst + trIdx * 8);
|
||||
if (trComp != 0)
|
||||
{
|
||||
var tr = mem.Read<Transitionable>(trComp);
|
||||
entity.TransitionState = tr.CurrentStateEnum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public class GameMemoryReader : IDisposable
|
|||
private MsvcStringReader? _strings;
|
||||
private RttiResolver? _rtti;
|
||||
private SkillReader? _skills;
|
||||
private QuestReader? _quests;
|
||||
|
||||
public ObjectRegistry Registry => _registry;
|
||||
public MemoryDiagnostics? Diagnostics { get; private set; }
|
||||
|
|
@ -99,6 +100,7 @@ public class GameMemoryReader : IDisposable
|
|||
_entities = new EntityReader(_ctx, _components, _strings);
|
||||
_terrain = new TerrainReader(_ctx);
|
||||
_skills = new SkillReader(_ctx, _components, _strings);
|
||||
_quests = new QuestReader(_ctx, _strings);
|
||||
Diagnostics = new MemoryDiagnostics(_ctx, _stateReader, _components, _entities, _strings, _rtti);
|
||||
|
||||
return true;
|
||||
|
|
@ -116,6 +118,7 @@ public class GameMemoryReader : IDisposable
|
|||
_strings = null;
|
||||
_rtti = null;
|
||||
_skills = null;
|
||||
_quests = null;
|
||||
Diagnostics = null;
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +222,9 @@ public class GameMemoryReader : IDisposable
|
|||
_components.InvalidateCaches(snap.LocalPlayerPtr);
|
||||
_components.ReadPlayerVitals(snap);
|
||||
_components.ReadPlayerPosition(snap);
|
||||
snap.CharacterName = _components.ReadPlayerName(snap.LocalPlayerPtr);
|
||||
snap.PlayerSkills = _skills!.ReadPlayerSkills(snap.LocalPlayerPtr);
|
||||
snap.QuestFlags = _quests!.ReadQuestFlags(snap.ServerDataPtr);
|
||||
}
|
||||
|
||||
// Read state flag bytes
|
||||
|
|
|
|||
|
|
@ -90,6 +90,24 @@ public sealed class GameOffsets
|
|||
// ServerData → fields
|
||||
/// <summary>ServerData → LocalPlayer entity pointer (fallback if LocalPlayerDirectOffset is 0).</summary>
|
||||
public int LocalPlayerOffset { get; set; } = 0x20;
|
||||
/// <summary>ServerData → PlayerServerData pointer (PerPlayerServerData struct).</summary>
|
||||
public int PlayerServerDataOffset { get; set; } = 0x50;
|
||||
/// <summary>PlayerServerData → QuestFlags container offset (PerPlayerServerDataOffsets: 0x230 = 560).</summary>
|
||||
public int QuestFlagsOffset { get; set; } = 0x230;
|
||||
/// <summary>Size of each quest flag entry in bytes. 0 = disabled (offsets not yet discovered via CE).</summary>
|
||||
public int QuestFlagEntrySize { get; set; } = 0;
|
||||
/// <summary>Offset within each quest entry to the Quest.dat row pointer.</summary>
|
||||
public int QuestEntryQuestPtrOffset { get; set; } = 0;
|
||||
/// <summary>Offset within each quest entry to the byte state ID.</summary>
|
||||
public int QuestEntryStateIdOffset { get; set; } = 0;
|
||||
/// <summary>Offset within each quest entry to the wchar* state text pointer.</summary>
|
||||
public int QuestEntryStateTextOffset { get; set; } = 0;
|
||||
/// <summary>Offset within each quest entry to the wchar* progress text pointer.</summary>
|
||||
public int QuestEntryProgressTextOffset { get; set; } = 0;
|
||||
/// <summary>Container type for quest flags: "vector" or "map".</summary>
|
||||
public string QuestFlagsContainerType { get; set; } = "vector";
|
||||
/// <summary>Maximum number of quest entries to read (sanity limit).</summary>
|
||||
public int QuestFlagsMaxEntries { get; set; } = 128;
|
||||
|
||||
// ── Entity / Component ──
|
||||
public int ComponentListOffset { get; set; } = 0x10;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ public class GameStateSnapshot
|
|||
public int AreaLevel;
|
||||
public uint AreaHash;
|
||||
|
||||
// Player
|
||||
public string? CharacterName;
|
||||
|
||||
// Player position (Render component)
|
||||
public bool HasPosition;
|
||||
public float PlayerX, PlayerY, PlayerZ;
|
||||
|
|
@ -63,6 +66,9 @@ public class GameStateSnapshot
|
|||
// Player skills (from Actor component)
|
||||
public List<SkillSnapshot>? PlayerSkills;
|
||||
|
||||
// Quest flags (from ServerData → PlayerServerData)
|
||||
public List<QuestSnapshot>? QuestFlags;
|
||||
|
||||
// Camera
|
||||
public Matrix4x4? CameraMatrix;
|
||||
|
||||
|
|
|
|||
|
|
@ -3834,4 +3834,288 @@ public sealed class MemoryDiagnostics
|
|||
result.AppendLine("Click again to capture a new baseline.");
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CE discovery diagnostic for quest flags. Follows the pointer chain:
|
||||
/// AreaInstance → ServerData → PlayerServerData (StdVector) → QuestFlags.
|
||||
/// When ServerData is null, dumps hex around the configured offset to help
|
||||
/// discover the correct one. PlayerServerDataPtr is a StdVector of pointers,
|
||||
/// so an extra dereference is needed.
|
||||
/// </summary>
|
||||
public string ScanQuestFlags()
|
||||
{
|
||||
if (_ctx.Memory is null) return "Error: not attached";
|
||||
if (_ctx.GameStateBase == 0) return "Error: GameState base not resolved";
|
||||
|
||||
var snap = new GameStateSnapshot();
|
||||
var inGameState = _stateReader.ResolveInGameState(snap);
|
||||
if (inGameState == 0) return "Error: InGameState not resolved";
|
||||
|
||||
var ingameData = _ctx.Memory.ReadPointer(inGameState + _ctx.Offsets.IngameDataFromStateOffset);
|
||||
if (ingameData == 0) return "Error: AreaInstance not resolved";
|
||||
|
||||
var mem = _ctx.Memory;
|
||||
var offsets = _ctx.Offsets;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"AreaInstance: 0x{ingameData:X}");
|
||||
|
||||
// Verify AreaInstance is valid by checking LocalPlayer
|
||||
var localPlayer = mem.ReadPointer(ingameData + offsets.LocalPlayerDirectOffset);
|
||||
sb.AppendLine($"LocalPlayer (+0x{offsets.LocalPlayerDirectOffset:X}): 0x{localPlayer:X} {(localPlayer != 0 ? "OK" : "NULL")}");
|
||||
|
||||
// ServerData pointer
|
||||
var serverData = mem.ReadPointer(ingameData + offsets.ServerDataOffset);
|
||||
sb.AppendLine($"ServerData (+0x{offsets.ServerDataOffset:X}): 0x{serverData:X}");
|
||||
|
||||
if (serverData == 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("ServerData is null at configured offset.");
|
||||
if (localPlayer != 0)
|
||||
sb.AppendLine("LocalPlayer IS valid — offset 0x9F0 may have shifted.");
|
||||
|
||||
// Scan AreaInstance around the expected region for heap pointers
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Scanning AreaInstance 0x980..0xA30 for heap pointers:");
|
||||
sb.AppendLine(new string('─', 80));
|
||||
|
||||
const int scanStart = 0x980;
|
||||
const int scanLen = 0xB0;
|
||||
var scanData = mem.ReadBytes(ingameData + scanStart, scanLen);
|
||||
if (scanData is not null)
|
||||
{
|
||||
for (var off = 0; off + 8 <= scanData.Length; off += 8)
|
||||
{
|
||||
var val = (nint)BitConverter.ToInt64(scanData, off);
|
||||
var absOff = scanStart + off;
|
||||
var hexBytes = BitConverter.ToString(scanData, off, 8).Replace("-", " ");
|
||||
var annotation = "";
|
||||
|
||||
if (val != 0)
|
||||
{
|
||||
var high = (ulong)val >> 32;
|
||||
if (high is > 0 and < 0x7FFF && (val & 0x3) == 0)
|
||||
{
|
||||
annotation = " [ptr]";
|
||||
// Check if target has a vtable
|
||||
var targetVtable = mem.ReadPointer(val);
|
||||
if (targetVtable != 0 && _ctx.IsModuleAddress(targetVtable))
|
||||
{
|
||||
var rtti = _rtti.ResolveRttiName(targetVtable);
|
||||
annotation = rtti is not null ? $" → vtable: {rtti}" : " → vtable (no RTTI)";
|
||||
}
|
||||
else if (targetVtable != 0)
|
||||
{
|
||||
// Check if target+0x50 looks like a StdVector (ServerData candidate)
|
||||
var t50_0 = mem.ReadPointer(val + 0x50);
|
||||
var t50_1 = mem.ReadPointer(val + 0x58);
|
||||
if (t50_0 != 0 && t50_1 > t50_0)
|
||||
annotation += $" → has StdVector at +0x50 (begin=0x{t50_0:X}, size={(int)(t50_1-t50_0)})";
|
||||
}
|
||||
}
|
||||
else if (val == localPlayer)
|
||||
{
|
||||
annotation = " = LocalPlayer";
|
||||
}
|
||||
}
|
||||
|
||||
var marker = absOff == offsets.ServerDataOffset ? " ◄ configured" : "";
|
||||
sb.AppendLine($" +0x{absOff:X3}: {hexBytes} (0x{val:X16}){annotation}{marker}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Look for a heap pointer with a StdVector at +0x50 — that's likely ServerData.");
|
||||
sb.AppendLine("Update ServerDataOffset in offsets.json and re-run.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine(new string('═', 80));
|
||||
|
||||
// PlayerServerData — ExileCore: ServerData+0x50 is StdVector (begin/end/cap)
|
||||
// The vector contains pointers to PerPlayerServerData structs
|
||||
var psdVecBegin = mem.ReadPointer(serverData + offsets.PlayerServerDataOffset);
|
||||
var psdVecEnd = mem.ReadPointer(serverData + offsets.PlayerServerDataOffset + 8);
|
||||
sb.AppendLine($"PlayerServerData StdVector (+0x{offsets.PlayerServerDataOffset:X}):");
|
||||
sb.AppendLine($" begin: 0x{psdVecBegin:X}");
|
||||
sb.AppendLine($" end: 0x{psdVecEnd:X}");
|
||||
|
||||
if (psdVecBegin == 0)
|
||||
{
|
||||
sb.AppendLine("Error: PlayerServerData vector begin is null");
|
||||
// Dump hex around ServerData+0x50 for discovery
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Hex dump at ServerData+0x40..0x70:");
|
||||
DumpHexRegion(sb, mem, serverData + 0x40, 0x30, 0x40);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Dereference: vector entry is a pointer to PerPlayerServerData
|
||||
var playerServerData = mem.ReadPointer(psdVecBegin);
|
||||
var vecEntries = psdVecEnd > psdVecBegin ? (int)(psdVecEnd - psdVecBegin) / 8 : 0;
|
||||
sb.AppendLine($" entries: {vecEntries} (vector of pointers, 8 bytes each)");
|
||||
sb.AppendLine($" [0] → PlayerServerData: 0x{playerServerData:X}");
|
||||
|
||||
if (playerServerData == 0)
|
||||
{
|
||||
sb.AppendLine("Error: PlayerServerData[0] pointer is null");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// QuestFlags region at PlayerServerData + 0x230
|
||||
var questFlagsAddr = playerServerData + offsets.QuestFlagsOffset;
|
||||
sb.AppendLine($"QuestFlags addr (+0x{offsets.QuestFlagsOffset:X}): 0x{questFlagsAddr:X}");
|
||||
sb.AppendLine(new string('═', 80));
|
||||
|
||||
// Dump 256 bytes at QuestFlags address
|
||||
const int dumpSize = 256;
|
||||
var regionData = mem.ReadBytes(questFlagsAddr, dumpSize);
|
||||
if (regionData is null)
|
||||
{
|
||||
sb.AppendLine("Error: failed to read QuestFlags region");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine($"\nHex dump at QuestFlags (0x{questFlagsAddr:X}), {dumpSize} bytes:");
|
||||
sb.AppendLine(new string('─', 80));
|
||||
|
||||
// Check for StdVector pattern (three ascending pointers at +0x00/+0x08/+0x10)
|
||||
nint vecBegin = 0, vecEnd = 0, vecCap = 0;
|
||||
bool isVector = false;
|
||||
|
||||
if (regionData.Length >= 24)
|
||||
{
|
||||
vecBegin = (nint)BitConverter.ToInt64(regionData, 0);
|
||||
vecEnd = (nint)BitConverter.ToInt64(regionData, 8);
|
||||
vecCap = (nint)BitConverter.ToInt64(regionData, 16);
|
||||
|
||||
if (vecBegin != 0 && vecEnd > vecBegin && vecCap >= vecEnd)
|
||||
{
|
||||
var high1 = (ulong)vecBegin >> 32;
|
||||
var high2 = (ulong)vecEnd >> 32;
|
||||
if (high1 is > 0 and < 0x7FFF && high2 is > 0 and < 0x7FFF)
|
||||
isVector = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Annotate each qword
|
||||
for (var off = 0; off + 8 <= regionData.Length; off += 8)
|
||||
{
|
||||
var val = (nint)BitConverter.ToInt64(regionData, off);
|
||||
var hexBytes = BitConverter.ToString(regionData, off, 8).Replace("-", " ");
|
||||
|
||||
var annotation = "";
|
||||
if (off == 0 && isVector) annotation = " ← vector.begin";
|
||||
else if (off == 8 && isVector) annotation = " ← vector.end";
|
||||
else if (off == 16 && isVector) annotation = " ← vector.capacity";
|
||||
else if (val != 0)
|
||||
{
|
||||
var high = (ulong)val >> 32;
|
||||
if (high is > 0 and < 0x7FFF && (val & 0x3) == 0)
|
||||
{
|
||||
annotation = " [heap ptr]";
|
||||
var targetVal = mem.ReadPointer(val);
|
||||
if (targetVal != 0 && _ctx.IsModuleAddress(targetVal))
|
||||
annotation += " → vtable";
|
||||
else
|
||||
{
|
||||
var str = _strings.ReadNullTermWString(val);
|
||||
if (str is not null)
|
||||
annotation += $" → \"{str}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($" +0x{off:X3}: {hexBytes} (0x{val:X16}){annotation}");
|
||||
}
|
||||
|
||||
// If vector pattern found, dump vector content
|
||||
if (isVector)
|
||||
{
|
||||
var vecSize = (int)(vecEnd - vecBegin);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(new string('═', 80));
|
||||
sb.AppendLine($"StdVector detected: begin=0x{vecBegin:X} end=0x{vecEnd:X} size={vecSize} bytes");
|
||||
|
||||
// Try common entry sizes
|
||||
foreach (var trySize in new[] { 8, 16, 24, 32, 40, 48, 56, 64 })
|
||||
{
|
||||
if (vecSize % trySize == 0)
|
||||
sb.AppendLine($" Divides evenly by {trySize}: {vecSize / trySize} entries");
|
||||
}
|
||||
|
||||
// Dump first 1024 bytes of vector content
|
||||
var contentSize = Math.Min(vecSize, 1024);
|
||||
var content = mem.ReadBytes(vecBegin, contentSize);
|
||||
if (content is not null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Vector content (first {contentSize} bytes):");
|
||||
sb.AppendLine(new string('─', 80));
|
||||
|
||||
for (var off = 0; off + 8 <= content.Length; off += 8)
|
||||
{
|
||||
var val = (nint)BitConverter.ToInt64(content, off);
|
||||
var hexBytes = BitConverter.ToString(content, off, 8).Replace("-", " ");
|
||||
|
||||
var annotation = "";
|
||||
if (val != 0)
|
||||
{
|
||||
var high = (ulong)val >> 32;
|
||||
if (high is > 0 and < 0x7FFF && (val & 0x3) == 0)
|
||||
{
|
||||
annotation = " [ptr]";
|
||||
var str = _strings.ReadNullTermWString(val);
|
||||
if (str is not null)
|
||||
annotation = $" → \"{str}\"";
|
||||
else
|
||||
{
|
||||
var namePtr = mem.ReadPointer(val);
|
||||
if (namePtr != 0)
|
||||
{
|
||||
var name = _strings.ReadNullTermWString(namePtr);
|
||||
if (name is not null)
|
||||
annotation = $" → dat? → \"{name}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (val > 0 && val < 256)
|
||||
{
|
||||
annotation = $" [byte-range: {(byte)val}]";
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($" vec+0x{off:X3}: {hexBytes} (0x{val:X16}){annotation}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No StdVector pattern detected at +0x00. Container may be a map or different layout.");
|
||||
sb.AppendLine("Try adjusting QuestFlagsOffset in offsets.json and re-running.");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(new string('═', 80));
|
||||
sb.AppendLine("Next steps:");
|
||||
sb.AppendLine("1. Accept a quest in-game, re-run this scan, compare vector size → derive entry size");
|
||||
sb.AppendLine("2. Look for heap pointers (Quest.dat row), byte values (state ID), wchar* (text)");
|
||||
sb.AppendLine("3. Update QuestFlagEntrySize and other quest offsets in offsets.json");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void DumpHexRegion(StringBuilder sb, ProcessMemory mem, nint addr, int size, int baseOffset)
|
||||
{
|
||||
var data = mem.ReadBytes(addr, size);
|
||||
if (data is null) { sb.AppendLine(" (read failed)"); return; }
|
||||
for (var off = 0; off + 8 <= data.Length; off += 8)
|
||||
{
|
||||
var val = (nint)BitConverter.ToInt64(data, off);
|
||||
var hexBytes = BitConverter.ToString(data, off, 8).Replace("-", " ");
|
||||
sb.AppendLine($" +0x{baseOffset + off:X3}: {hexBytes} (0x{val:X16})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
183
src/Roboto.Memory/QuestReader.cs
Normal file
183
src/Roboto.Memory/QuestReader.cs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight quest data from ServerData quest flags.
|
||||
/// Stored in GameStateSnapshot; mapped to Roboto.Core.QuestProgress in the Data layer.
|
||||
/// </summary>
|
||||
public sealed class QuestSnapshot
|
||||
{
|
||||
public nint QuestDatPtr { get; init; }
|
||||
public string? QuestName { get; init; }
|
||||
public byte StateId { get; init; }
|
||||
public string? StateText { get; init; }
|
||||
public string? ProgressText { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads quest flags from ServerData → PlayerServerData → QuestFlags vector.
|
||||
/// Follows the same pattern as SkillReader: bulk-reads vector data, resolves
|
||||
/// quest names by following dat row pointers, caches results.
|
||||
/// When QuestFlagEntrySize == 0 (offsets not yet discovered), gracefully returns null.
|
||||
/// </summary>
|
||||
public sealed class QuestReader
|
||||
{
|
||||
private readonly MemoryContext _ctx;
|
||||
private readonly MsvcStringReader _strings;
|
||||
|
||||
// Name cache — quest names are static, only refresh on ServerData change
|
||||
private readonly Dictionary<nint, string?> _nameCache = new();
|
||||
private nint _lastServerData;
|
||||
|
||||
public QuestReader(MemoryContext ctx, MsvcStringReader strings)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_strings = strings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads quest flags from the ServerData pointer chain.
|
||||
/// Returns null if offsets are not configured (EntrySize == 0) or data is unavailable.
|
||||
/// </summary>
|
||||
public List<QuestSnapshot>? ReadQuestFlags(nint serverDataPtr)
|
||||
{
|
||||
if (serverDataPtr == 0) return null;
|
||||
|
||||
var offsets = _ctx.Offsets;
|
||||
|
||||
// Guard: entry size 0 means offsets not yet discovered via CE
|
||||
if (offsets.QuestFlagEntrySize <= 0) return null;
|
||||
|
||||
var mem = _ctx.Memory;
|
||||
|
||||
// ServerData+0x50 is a StdVector of pointers to PerPlayerServerData structs.
|
||||
// Read vector begin, then dereference to get the first entry.
|
||||
var psdVecBegin = mem.ReadPointer(serverDataPtr + offsets.PlayerServerDataOffset);
|
||||
if (psdVecBegin == 0) return null;
|
||||
|
||||
// Dereference: vector[0] is a pointer to the actual PerPlayerServerData struct
|
||||
var playerServerData = mem.ReadPointer(psdVecBegin);
|
||||
if (playerServerData == 0) return null;
|
||||
|
||||
// Invalidate name cache on ServerData change (area transition)
|
||||
if (playerServerData != _lastServerData)
|
||||
{
|
||||
_nameCache.Clear();
|
||||
_lastServerData = playerServerData;
|
||||
}
|
||||
|
||||
// PerPlayerServerData → QuestFlags (+0x230)
|
||||
var questFlagsAddr = playerServerData + offsets.QuestFlagsOffset;
|
||||
|
||||
if (offsets.QuestFlagsContainerType == "vector")
|
||||
return ReadVectorQuests(questFlagsAddr, offsets);
|
||||
|
||||
// Future: "map" container type
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<QuestSnapshot>? ReadVectorQuests(nint questFlagsAddr, GameOffsets offsets)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
|
||||
// StdVector: begin, end, capacity (3 pointers)
|
||||
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;
|
||||
|
||||
// Bulk read all entries
|
||||
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;
|
||||
|
||||
// Read quest dat pointer
|
||||
nint questDatPtr = 0;
|
||||
if (entryOffset + offsets.QuestEntryQuestPtrOffset + 8 <= vecData.Length)
|
||||
questDatPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryQuestPtrOffset);
|
||||
|
||||
// Read state ID byte
|
||||
byte stateId = 0;
|
||||
if (entryOffset + offsets.QuestEntryStateIdOffset < vecData.Length)
|
||||
stateId = vecData[entryOffset + offsets.QuestEntryStateIdOffset];
|
||||
|
||||
// Resolve quest name from dat pointer (cached)
|
||||
var questName = ResolveQuestName(questDatPtr);
|
||||
|
||||
// Read state text pointer and resolve
|
||||
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);
|
||||
}
|
||||
|
||||
// Read progress text pointer and resolve
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves quest name by following QuestDatPtr → dat row → wchar* name.
|
||||
/// Results are cached since quest names don't change.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
// Follow the dat row pointer — first field is typically a wchar* name
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Clears cached names (call on area change).</summary>
|
||||
public void InvalidateCache()
|
||||
{
|
||||
_nameCache.Clear();
|
||||
_lastServerData = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue