603 lines
21 KiB
C#
603 lines
21 KiB
C#
using System.Text;
|
|
|
|
namespace Roboto.Memory.Objects;
|
|
|
|
/// <summary>
|
|
/// Reads the UIElement tree from InGameState → UiRootStruct → GameUi.
|
|
/// Fully lazy: ReadData() only resolves root pointers (2 RPM).
|
|
/// Tree nodes are read on-demand via ReadNode/ReadChildren from the UI layer.
|
|
/// </summary>
|
|
public sealed class UIElements : RemoteObject
|
|
{
|
|
// Bulk-read covers offsets 0x00 through 0x468 (Text std::wstring at 0x448 + 32 bytes).
|
|
private const int BulkReadSize = 0x468;
|
|
|
|
/// <summary>Maximum children to read per node (safety limit).</summary>
|
|
public int MaxChildrenPerNode { get; set; } = 200;
|
|
|
|
public nint UiRootPtr { get; private set; }
|
|
public nint GameUiPtr { get; private set; }
|
|
|
|
private readonly MsvcStringReader _strings;
|
|
|
|
// Cached quest parent pointers — resolved once, reused across reads
|
|
private nint _cachedTrackedPanelAddr; // GameUi[6][1]
|
|
private nint _cachedQuestParentAddr; // GameUi[6][1][0][0][0]
|
|
private nint _cachedForGameUi; // GameUiPtr when cache was built
|
|
|
|
/// <summary>Optional lookup for resolving quest state IDs to human-readable text.</summary>
|
|
public QuestStateLookup? QuestStateLookup { get; set; }
|
|
|
|
/// <summary>Optional files container for resolving MapPins → WorldAreas.</summary>
|
|
public FilesContainer? FilesContainer { get; set; }
|
|
|
|
/// <summary>Current area's WorldAreas.dat row address — set by GameMemoryReader for path resolution.</summary>
|
|
public nint CurrentAreaRowAddress { get; set; }
|
|
|
|
public UIElements(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
|
|
{
|
|
_strings = strings;
|
|
}
|
|
|
|
protected override bool ReadData()
|
|
{
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
// Address = InGameState pointer
|
|
// Follow: InGameState+UiRootStructOffset → UiRootStruct ptr
|
|
var uiRootStructPtr = mem.ReadPointer(Address + offsets.UiRootStructOffset);
|
|
if (uiRootStructPtr == 0 || (ulong)uiRootStructPtr >> 32 is 0 or >= 0x7FFF)
|
|
{
|
|
UiRootPtr = 0;
|
|
GameUiPtr = 0;
|
|
return true;
|
|
}
|
|
|
|
// UiRootStruct → UiRoot and GameUi element pointers
|
|
UiRootPtr = mem.ReadPointer(uiRootStructPtr + offsets.UiRootPtrOffset);
|
|
GameUiPtr = mem.ReadPointer(uiRootStructPtr + offsets.GameUiPtrOffset);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a single UIElement node (1 bulk RPM). Returns null if address is invalid.
|
|
/// Does NOT read children — call ReadChildren separately.
|
|
/// </summary>
|
|
public UIElementNode? ReadNode(nint addr)
|
|
{
|
|
if (addr == 0 || (ulong)addr >> 32 is 0 or >= 0x7FFF)
|
|
return null;
|
|
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
var buf = mem.ReadBytes(addr, BulkReadSize);
|
|
if (buf is null || buf.Length < BulkReadSize)
|
|
return null;
|
|
|
|
// Self-check validation
|
|
var selfPtr = (nint)BitConverter.ToInt64(buf, offsets.UiElementSelfOffset);
|
|
if (selfPtr != addr)
|
|
return null;
|
|
|
|
var stringId = ParseWStringFromBuffer(buf, offsets.UiElementStringIdOffset);
|
|
var text = ParseWStringFromBuffer(buf, offsets.UiElementTextOffset);
|
|
|
|
var flags = BitConverter.ToUInt32(buf, offsets.UiElementFlagsOffset);
|
|
var isVisible = (flags & (1u << offsets.UiElementVisibleBit)) != 0;
|
|
|
|
var width = BitConverter.ToSingle(buf, offsets.UiElementSizeOffset);
|
|
var height = BitConverter.ToSingle(buf, offsets.UiElementSizeOffset + 4);
|
|
|
|
// Children count (don't read children themselves)
|
|
var childCount = 0;
|
|
var vecBegin = (nint)BitConverter.ToInt64(buf, offsets.UiElementChildrenOffset);
|
|
var vecEnd = (nint)BitConverter.ToInt64(buf, offsets.UiElementChildrenOffset + 8);
|
|
if (vecBegin != 0 && vecEnd > vecBegin)
|
|
{
|
|
childCount = (int)(vecEnd - vecBegin) / 8;
|
|
if (childCount > 10000) childCount = 0;
|
|
}
|
|
|
|
return new UIElementNode
|
|
{
|
|
Address = addr,
|
|
StringId = stringId,
|
|
Text = text,
|
|
IsVisible = isVisible,
|
|
Width = width,
|
|
Height = height,
|
|
ChildCount = childCount,
|
|
Children = null,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// On-demand: reads immediate children of a node address.
|
|
/// Each child is a shallow UIElementNode (no grandchildren).
|
|
/// </summary>
|
|
public List<UIElementNode>? ReadChildren(nint nodeAddr)
|
|
{
|
|
if (nodeAddr == 0) return null;
|
|
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
var vecBegin = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset);
|
|
var vecEnd = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset + 8);
|
|
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
|
|
|
|
var childCount = (int)(vecEnd - vecBegin) / 8;
|
|
if (childCount <= 0 || childCount > 10000) return null;
|
|
|
|
var count = Math.Min(childCount, MaxChildrenPerNode);
|
|
var ptrData = mem.ReadBytes(vecBegin, count * 8);
|
|
if (ptrData is null) return null;
|
|
|
|
var result = new List<UIElementNode>(count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var childPtr = (nint)BitConverter.ToInt64(ptrData, i * 8);
|
|
var child = ReadNode(childPtr);
|
|
if (child is not null)
|
|
result.Add(child);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// On-demand text read for a specific node address (1-2 RPM).
|
|
/// </summary>
|
|
public string? ReadNodeText(nint nodeAddr)
|
|
{
|
|
if (nodeAddr == 0) return null;
|
|
return ReadUiWString(nodeAddr + Ctx.Offsets.UiElementTextOffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the Nth child of a node (0-indexed). Returns null if out of range or invalid.
|
|
/// </summary>
|
|
public UIElementNode? ReadChildAtIndex(nint nodeAddr, int index)
|
|
{
|
|
if (nodeAddr == 0 || index < 0) return null;
|
|
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
var vecBegin = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset);
|
|
var vecEnd = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset + 8);
|
|
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
|
|
|
|
var childCount = (int)(vecEnd - vecBegin) / 8;
|
|
if (index >= childCount) return null;
|
|
|
|
var childPtr = mem.ReadPointer(vecBegin + index * 8);
|
|
return ReadNode(childPtr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Navigates a path of child indices from a starting node address.
|
|
/// e.g. NavigatePath(gameUiPtr, [6, 1, 0, 0, 0, 2]) → root[6][1][0][0][0][2]
|
|
/// </summary>
|
|
public UIElementNode? NavigatePath(nint startAddr, ReadOnlySpan<int> path)
|
|
{
|
|
var current = startAddr;
|
|
UIElementNode? node = null;
|
|
|
|
foreach (var idx in path)
|
|
{
|
|
node = ReadChildAtIndex(current, idx);
|
|
if (node is null) return null;
|
|
current = node.Address;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves and caches quest parent pointers from the UI tree.
|
|
/// Called lazily; cache invalidated when GameUiPtr changes.
|
|
/// </summary>
|
|
private void EnsureQuestPointerCache()
|
|
{
|
|
if (_cachedForGameUi == GameUiPtr && _cachedTrackedPanelAddr != 0)
|
|
return; // cache is valid
|
|
|
|
_cachedForGameUi = GameUiPtr;
|
|
_cachedTrackedPanelAddr = 0;
|
|
_cachedQuestParentAddr = 0;
|
|
|
|
if (GameUiPtr == 0) return;
|
|
|
|
var offsets = Ctx.Offsets;
|
|
|
|
// GameUi[6] → [1] for tracked quests
|
|
var elem6 = ReadChildAtIndex(GameUiPtr, offsets.TrackedQuestPanelChildIndex);
|
|
if (elem6 is not null)
|
|
{
|
|
var elem61 = ReadChildAtIndex(elem6.Address, offsets.TrackedQuestPanelSubChildIndex);
|
|
if (elem61 is not null)
|
|
_cachedTrackedPanelAddr = elem61.Address;
|
|
}
|
|
|
|
// GameUi[6][1][0][0][0] for quest groups
|
|
var questParent = NavigatePath(GameUiPtr, [6, 1, 0, 0, 0]);
|
|
if (questParent is not null)
|
|
_cachedQuestParentAddr = questParent.Address;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads quest groups from the UI tree.
|
|
/// Path: GameUi[6][1][0][0][0] → quest_display → [0] → title_layout/quest_info_layout
|
|
/// </summary>
|
|
public List<UiQuestGroup>? ReadQuestGroups()
|
|
{
|
|
if (GameUiPtr == 0) return null;
|
|
|
|
EnsureQuestPointerCache();
|
|
if (_cachedQuestParentAddr == 0) return null;
|
|
|
|
var questDisplays = ReadChildren(_cachedQuestParentAddr);
|
|
if (questDisplays is null) return null;
|
|
|
|
var groups = new List<UiQuestGroup>();
|
|
foreach (var qd in questDisplays)
|
|
{
|
|
if (!string.Equals(qd.StringId, "quest_display", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
// quest_display → [0] (unnamed child with title_layout + quest_info_layout)
|
|
var qdChildren = ReadChildren(qd.Address);
|
|
if (qdChildren is null || qdChildren.Count == 0) continue;
|
|
|
|
var innerChildren = ReadChildren(qdChildren[0].Address);
|
|
if (innerChildren is null) continue;
|
|
|
|
var group = new UiQuestGroup();
|
|
|
|
foreach (var child in innerChildren)
|
|
{
|
|
if (string.Equals(child.StringId, "title_layout", StringComparison.Ordinal))
|
|
{
|
|
// title_layout → title_label (has quest name text)
|
|
var titleChildren = ReadChildren(child.Address);
|
|
if (titleChildren is not null)
|
|
{
|
|
foreach (var tc in titleChildren)
|
|
{
|
|
if (string.Equals(tc.StringId, "title_label", StringComparison.Ordinal))
|
|
group.Title = tc.Text;
|
|
}
|
|
}
|
|
}
|
|
else if (string.Equals(child.StringId, "quest_info_layout", StringComparison.Ordinal))
|
|
{
|
|
ReadQuestSteps(child.Address, group.Steps);
|
|
}
|
|
}
|
|
|
|
if (group.Title is not null || group.Steps.Count > 0)
|
|
groups.Add(group);
|
|
}
|
|
|
|
return groups.Count > 0 ? groups : null;
|
|
}
|
|
|
|
private void ReadQuestSteps(nint layoutAddr, List<UiQuestStep> steps)
|
|
{
|
|
var layoutChildren = ReadChildren(layoutAddr);
|
|
if (layoutChildren is null) return;
|
|
|
|
foreach (var entryNode in layoutChildren)
|
|
{
|
|
if (!string.Equals(entryNode.StringId, "quest_info_entry", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
var entryChildren = ReadChildren(entryNode.Address);
|
|
if (entryChildren is null) continue;
|
|
|
|
var step = new UiQuestStep();
|
|
foreach (var part in entryChildren)
|
|
{
|
|
if (string.Equals(part.StringId, "quest_info", StringComparison.Ordinal))
|
|
step.Text = part.Text;
|
|
}
|
|
|
|
if (step.Text is not null)
|
|
steps.Add(step);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// BFS search: reads nodes on-demand until a matching StringId is found.
|
|
/// Walks the live game memory — use sparingly.
|
|
/// </summary>
|
|
public UIElementNode? FindByStringId(string id)
|
|
{
|
|
if (GameUiPtr == 0) return null;
|
|
|
|
var root = ReadNode(GameUiPtr);
|
|
if (root is null) return null;
|
|
if (string.Equals(root.StringId, id, StringComparison.Ordinal))
|
|
return root;
|
|
|
|
var queue = new Queue<nint>();
|
|
var visited = new HashSet<nint> { GameUiPtr };
|
|
EnqueueChildren(queue, visited, GameUiPtr);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var addr = queue.Dequeue();
|
|
var node = ReadNode(addr);
|
|
if (node is null) continue;
|
|
|
|
if (string.Equals(node.StringId, id, StringComparison.Ordinal))
|
|
return node;
|
|
|
|
EnqueueChildren(queue, visited, addr);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads both quest linked lists (all-quests + tracked) and merges them.
|
|
/// Returns null if GameUi is not available.
|
|
/// </summary>
|
|
public List<QuestLinkedEntry>? ReadQuestLinkedLists()
|
|
{
|
|
if (GameUiPtr == 0) return null;
|
|
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
EnsureQuestPointerCache();
|
|
|
|
// ── Tracked quests: cached [6][1]+0x318 — collect into dict keyed by QuestDatPtr ──
|
|
var trackedMap = new Dictionary<nint, string?>();
|
|
if (_cachedTrackedPanelAddr != 0)
|
|
{
|
|
var trackedHead = mem.ReadPointer(_cachedTrackedPanelAddr + offsets.TrackedQuestLinkedListOffset);
|
|
if (trackedHead != 0)
|
|
TraverseTrackedQuests(trackedHead, trackedMap);
|
|
}
|
|
|
|
// ── All quests: GameUi+0x358 ──
|
|
var allHead = mem.ReadPointer(GameUiPtr + offsets.QuestLinkedListOffset);
|
|
if (allHead == 0) return null;
|
|
|
|
return TraverseAllQuests(allHead, trackedMap);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks the all-quests linked list. Reads Quest.dat row fields + stateId per node.
|
|
/// Merges tracked info from the trackedMap.
|
|
/// </summary>
|
|
private List<QuestLinkedEntry>? TraverseAllQuests(nint headPtr, Dictionary<nint, string?> trackedMap)
|
|
{
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
var maxNodes = offsets.QuestLinkedListMaxNodes;
|
|
var readSize = Math.Max(offsets.QuestLinkedListNodeSize, 48);
|
|
|
|
var visited = new HashSet<nint>();
|
|
var walk = headPtr;
|
|
var isSentinel = true;
|
|
var result = new List<QuestLinkedEntry>();
|
|
|
|
while (result.Count < maxNodes)
|
|
{
|
|
if (walk == 0 || !visited.Add(walk)) break;
|
|
|
|
var nodeData = mem.ReadBytes(walk, readSize);
|
|
if (nodeData is null) break;
|
|
|
|
var next = (nint)BitConverter.ToInt64(nodeData, 0);
|
|
|
|
if (isSentinel)
|
|
{
|
|
isSentinel = false;
|
|
walk = next;
|
|
continue;
|
|
}
|
|
|
|
var questPtr = (nint)BitConverter.ToInt64(nodeData, offsets.QuestNodeQuestPtrOffset);
|
|
var stateId = BitConverter.ToInt32(nodeData, offsets.QuestNodeStateIdOffset);
|
|
|
|
string? internalId = null;
|
|
string? displayName = null;
|
|
var act = -1;
|
|
|
|
if (questPtr != 0 && ((ulong)questPtr >> 32) is > 0 and < 0x7FFF)
|
|
{
|
|
var idPtr = mem.ReadPointer(questPtr + offsets.QuestObjNamePtrOffset);
|
|
if (idPtr != 0 && ((ulong)idPtr >> 32) is > 0 and < 0x7FFF)
|
|
internalId = _strings.ReadNullTermWString(idPtr);
|
|
|
|
act = mem.Read<int>(questPtr + offsets.QuestObjActOffset);
|
|
|
|
var namePtr = mem.ReadPointer(questPtr + offsets.QuestObjDisplayNameOffset);
|
|
if (namePtr != 0 && ((ulong)namePtr >> 32) is > 0 and < 0x7FFF)
|
|
displayName = _strings.ReadNullTermWString(namePtr);
|
|
}
|
|
|
|
var isTracked = trackedMap.ContainsKey(questPtr);
|
|
|
|
string? stateText = null;
|
|
string? mapPinsText = null;
|
|
List<ConnectedAreaInfo>? targetAreas = null;
|
|
List<string>? pathToTarget = null;
|
|
|
|
if (QuestStateLookup is not null && questPtr != 0 && stateId > 0)
|
|
{
|
|
QuestStateLookup.TryGetStateText(questPtr, stateId, out stateText);
|
|
|
|
// Resolve target areas via MapPins → WorldAreas
|
|
if (FilesContainer is not null)
|
|
{
|
|
var questStateRow = QuestStateLookup.GetQuestStateRow(questPtr, stateId);
|
|
if (questStateRow is not null)
|
|
{
|
|
mapPinsText = questStateRow.MapPinsText;
|
|
var worldAreas = FilesContainer.GetMapPinWorldAreas(questStateRow);
|
|
if (worldAreas.Count > 0)
|
|
{
|
|
targetAreas = worldAreas.Select(wa => new ConnectedAreaInfo
|
|
{
|
|
Id = wa.Id,
|
|
Name = wa.Name,
|
|
Act = wa.Act,
|
|
IsTown = wa.IsTown,
|
|
HasWaypoint = wa.HasWaypoint,
|
|
MonsterLevel = wa.MonsterLevel,
|
|
WorldAreaId = wa.WorldAreaId,
|
|
}).ToList();
|
|
|
|
// BFS path from current area to first target area
|
|
if (CurrentAreaRowAddress != 0)
|
|
{
|
|
var currentArea = FilesContainer.WorldAreas.GetByAddress(CurrentAreaRowAddress);
|
|
if (currentArea is not null)
|
|
{
|
|
var path = FilesContainer.FindPath(currentArea, worldAreas[0]);
|
|
if (path is { Count: > 1 })
|
|
pathToTarget = path.Select(p => p.Name ?? p.Id ?? "?").ToList();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Add(new QuestLinkedEntry
|
|
{
|
|
InternalId = internalId,
|
|
DisplayName = displayName,
|
|
Act = act,
|
|
StateId = stateId,
|
|
StateText = stateText,
|
|
IsTracked = isTracked,
|
|
QuestDatPtr = questPtr,
|
|
MapPinsText = mapPinsText,
|
|
TargetAreas = targetAreas,
|
|
PathToTarget = pathToTarget,
|
|
});
|
|
|
|
walk = next;
|
|
}
|
|
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks the tracked-quests linked list. Builds a dict of QuestDatPtr → null.
|
|
/// Tracked status is used; objective text is not available from runtime memory
|
|
/// (stateObj contains quest flag arrays, not display text).
|
|
/// </summary>
|
|
private void TraverseTrackedQuests(nint headPtr, Dictionary<nint, string?> trackedMap)
|
|
{
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
var maxNodes = offsets.QuestLinkedListMaxNodes;
|
|
var readSize = Math.Max(offsets.QuestLinkedListNodeSize, 48);
|
|
|
|
var visited = new HashSet<nint>();
|
|
var walk = headPtr;
|
|
var isSentinel = true;
|
|
var count = 0;
|
|
|
|
while (count < maxNodes)
|
|
{
|
|
if (walk == 0 || !visited.Add(walk)) break;
|
|
|
|
var nodeData = mem.ReadBytes(walk, readSize);
|
|
if (nodeData is null) break;
|
|
|
|
var next = (nint)BitConverter.ToInt64(nodeData, 0);
|
|
|
|
if (isSentinel)
|
|
{
|
|
isSentinel = false;
|
|
walk = next;
|
|
continue;
|
|
}
|
|
|
|
var questPtr = (nint)BitConverter.ToInt64(nodeData, offsets.QuestNodeQuestPtrOffset);
|
|
|
|
if (questPtr != 0)
|
|
trackedMap[questPtr] = null;
|
|
|
|
count++;
|
|
walk = next;
|
|
}
|
|
}
|
|
|
|
private void EnqueueChildren(Queue<nint> queue, HashSet<nint> visited, nint parentAddr)
|
|
{
|
|
var mem = Ctx.Memory;
|
|
var offsets = Ctx.Offsets;
|
|
|
|
var vecBegin = mem.ReadPointer(parentAddr + offsets.UiElementChildrenOffset);
|
|
var vecEnd = mem.ReadPointer(parentAddr + offsets.UiElementChildrenOffset + 8);
|
|
if (vecBegin == 0 || vecEnd <= vecBegin) return;
|
|
|
|
var count = Math.Min((int)(vecEnd - vecBegin) / 8, MaxChildrenPerNode);
|
|
if (count <= 0) return;
|
|
|
|
var data = mem.ReadBytes(vecBegin, count * 8);
|
|
if (data is null) return;
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var ptr = (nint)BitConverter.ToInt64(data, i * 8);
|
|
if (ptr != 0 && visited.Add(ptr))
|
|
queue.Enqueue(ptr);
|
|
}
|
|
}
|
|
|
|
private string? ParseWStringFromBuffer(byte[] buf, int offset)
|
|
{
|
|
if (offset + 32 > buf.Length) return null;
|
|
|
|
var length = BitConverter.ToInt32(buf, offset + 0x10);
|
|
var capacity = BitConverter.ToInt32(buf, offset + 0x18);
|
|
if (length <= 0 || length > 4096 || capacity < length) return null;
|
|
|
|
try
|
|
{
|
|
if (capacity <= 8)
|
|
{
|
|
var byteLen = Math.Min(length * 2, 16);
|
|
return Encoding.Unicode.GetString(buf, offset, byteLen).TrimEnd('\0');
|
|
}
|
|
else
|
|
{
|
|
var ptr = (nint)BitConverter.ToInt64(buf, offset);
|
|
if (ptr == 0 || (ulong)ptr >> 32 is 0 or >= 0x7FFF) return null;
|
|
var charData = Ctx.Memory.ReadBytes(ptr, Math.Min(length * 2, 512));
|
|
if (charData is null) return null;
|
|
return Encoding.Unicode.GetString(charData).TrimEnd('\0');
|
|
}
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
private string? ReadUiWString(nint addr)
|
|
{
|
|
var strData = Ctx.Memory.ReadBytes(addr, 32);
|
|
if (strData is null || strData.Length < 28) return null;
|
|
return ParseWStringFromBuffer(strData, 0);
|
|
}
|
|
|
|
protected override void Clear()
|
|
{
|
|
UiRootPtr = 0;
|
|
GameUiPtr = 0;
|
|
_cachedForGameUi = 0;
|
|
_cachedTrackedPanelAddr = 0;
|
|
_cachedQuestParentAddr = 0;
|
|
}
|
|
}
|