diff --git a/README.md b/README.md index 6fc8ca8..cbc3afb 100644 --- a/README.md +++ b/README.md @@ -12,64 +12,143 @@ [![Build](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml/badge.svg)](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml) [![License](https://img.shields.io/github/license/IChooseYou/Reclass)](LICENSE) [![Release](https://img.shields.io/github/v/release/IChooseYou/Reclass?label=snapshot)](https://github.com/IChooseYou/Reclass/releases) -[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue)]() +[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue)]() -Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump. +Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump. Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews. ## Features -- **Structured binary view** — render raw bytes as typed fields (integers, floats, pointers, vectors, matrices, strings, booleans, padding) -- **Struct & array nesting** — define nested structs and arrays with collapsible fold regions -- **Enums & bitfields** — define enums and bitfield types with named members, inline editing, and auto-sort -- **Inline editing** — click to edit type names, field names, values, and base addresses directly in the editor -- **Undo/redo** — full undo history for all mutations via command stack -- **Multi-document tabs** — open multiple projects simultaneously in MDI sub-windows +### Type System — 25 Data Types + +| Category | Types | +|----------|-------| +| **Hex preview** | Hex8, Hex16, Hex32, Hex64 | +| **Integers** | Int8/16/32/64, UInt8/16/32/64 | +| **Floating point** | Float, Double | +| **Boolean** | Bool | +| **Pointers** | Pointer32, Pointer64, FuncPtr32, FuncPtr64 | +| **Vectors & matrices** | Vec2, Vec3, Vec4, Mat4x4 | +| **Strings** | UTF8, UTF16 (length-aware) | +| **Containers** | Struct (class/struct/union/enum keywords), Array (typed elements with viewport scrolling) | + +### Editor + +- **Structured binary view** — render raw bytes as typed fields with columnar alignment +- **Inline editing** — click to edit type names, field names, values, base addresses, array metadata, pointer targets, enum members, bitfield members, static expressions, and comments — all with real-time validation +- **Tab-cycling** — tab through editable fields within a line +- **Type autocomplete** — cached popup type picker with search/filter for struct targets +- **Multi-select** — Ctrl+click individual nodes or Shift+click for range selection - **Split views** — multiple synchronized editor panes over the same document -- **Type autocomplete** — popup type picker when changing field kinds -- **Hex + ASCII margins** — raw byte previews alongside the structured view -- **Value history & heatmap** — track value changes over time with color-coded heat indicators -- **Disassembly preview** — hover over code pointers to see decoded instructions -- **C/C++ code generation** — export structs as compilable C/C++ headers -- **Import / export** — PDB import (Windows), ReClass XML import/export, C/C++ source import -- **Themes** — built-in theme editor with multiple presets -- **MCP bridge** — expose all tool functionality to AI clients via Model Context Protocol -- **Plugin system** — extend with custom data source providers via DLL plugins; the following ship by default: - - **Process plugin** — access memory of live processes on Windows and Linux - - **WinDbg plugin** — access data sources live in WinDbg debugging sessions - - **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins +- **Find bar** — Ctrl+F in-editor search with indicator highlighting +- **Fold/collapse** — expand and collapse structs, arrays, and pointer expansions with embedded fold indicators +- **Hex + ASCII columns** — raw byte previews alongside the structured view with per-byte change highlighting -## Roadmap +### Struct & Container Support -- [ ] Process memory section enumeration -- [ ] Address parser auto-complete -- [ ] Safe mode -- [ ] File import for other Reclass instances -- [ ] Expose UI functionality to plugins -- [ ] iOS support -- [ ] Display RTTI information +- **Struct nesting** — define nested structs and arrays with collapsible fold regions +- **Enums** — define enums with named value members, inline editing, and auto-sort +- **Bitfields** — named bit-range members within structs, per-bit toggle, masked extraction +- **Unions** — group nodes into unions, dissolve unions back to flat fields +- **Pointer virtual expansion** — pointer nodes auto-dereference and inline-expand the target struct's fields, with cycle detection to prevent infinite recursion +- **Cross-document type resolution** — pointer targets resolve across all open tabs +- **Static fields** — C/C++ expression-evaluated offsets for globals, vtable entries, and computed addresses + +### Live Memory Analysis + +- **Auto-refresh** — configurable interval (default 660ms) with async page-based reads for non-blocking UI +- **Value history & heatmap** — per-node ring buffer (10 samples with timestamps), color-coded heat indicators (static/cold/warm/hot) based on change frequency +- **Changed-byte highlighting** — per-byte change indicators within hex preview lines +- **Memory write-back** — edit values inline, writes propagate through the provider to live process memory +- **Pointer chasing** — automatic reads of dereferenced memory regions across pointer chains +- **Address parser** — formula expressions like `+0x1A0`, pointer dereference chains, symbol resolution + +### Visualization + +- **Tree line connectors** — Unicode box-drawing characters (│ ├─ └─) at arbitrary nesting depth +- **Compact column mode** — caps type column width, overflows long type names +- **Relative / absolute offsets** — toggle inline offset display in the indent area +- **Disassembly preview** — hover over code pointers to see decoded x86 instructions (32/64-bit via Fadec) +- **Themes** — 5 built-in themes (ReClass Dark, VS Light, Warm, Midtone, Tailwind), live theme editor with 25-color customization, JSON import/export + +### Undo / Redo + +Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, Insert, Remove, ChangeBase, WriteBytes, ChangeArrayMeta, ChangePointerRef, ChangeStructTypeName, ChangeClassKeyword, ChangeOffset, ChangeEnumMembers, ChangeOffsetExpr, ToggleStatic. Batch macro support for multi-node operations. + +### Scanner / Memory Search + +- **Signature scanning** — IDA-style pattern matching (`48 8B ?? 05`) +- **Typed value search** — Int8-64, UInt8-64, Float, Double, Vec2/3/4, UTF8, UTF16, HexBytes +- **Scan conditions** — ExactValue, UnknownValue, Changed, Unchanged, Increased, Decreased +- **Region filtering** — filter by executable, writable, or struct-only regions +- **Alignment control** — 1, 4, or 8-byte alignment +- **Async multi-threaded** — progress bar, abort capability, up to 50K results +- **Rescan** — refine results with condition-based filtering + +### Import / Export + +| Format | Import | Export | +|--------|:------:|:------:| +| **Native JSON (.rcx)** | Full tree + metadata | Full tree + metadata | +| **C/C++ source** | Struct/class/union/enum parsing with offset comments | Header generation with optional static asserts | +| **ReClass XML** | Full compatibility with ReClass Classic | Full compatibility | +| **PDB symbols (Windows)** | UDT enumeration with selective recursive import via raw_pdb — no DIA SDK dependency | — | +| **Binary files** | Raw file loading as memory buffer | — | + +### Workspace & Navigation + +- **Multi-document tabs** — MDI interface, one document per tab +- **Workspace dock** — project explorer tree with struct/enum/union icons, sorted by field count, quick navigation to members +- **Scanner dock** — integrated memory search panel +- **Dual view mode** — switch between ReClass tree view and rendered C/C++ output per tab +- **View root** — focus on a specific struct, hiding all others +- **Scroll to node** — programmatic navigation to any node by ID ## Data Sources - **File** — open any binary file and inspect its contents as structured data -- **Process** — attach to a live process and read its memory in real time -- **Remote Process** — read another process's memory via shared memory -- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions +- **Process** — attach to a live process and read its memory in real time (Windows/Linux) +- **Remote Process** — read another process's memory over TCP with cross-architecture 32/64-bit support +- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps +- **Saved sources** — quick-switch between recently used data sources per tab -## Screenshots +## Plugin System -![Type chooser and struct inspection](docs/README_PIC1.png) +Extensible provider architecture via DLL plugins with `IPlugin` interface, factory function discovery, and auto/manual loading from a Plugins folder. -![VTable pointer expansion with disassembly preview](docs/README_PIC2.png) +**Bundled plugins:** -![Split view with rendered C/C++ output](docs/README_PIC3.png) +| Plugin | Description | +|--------|-------------| +| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration | +| **WinDbg** | Access data from live WinDbg debugging sessions | +| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support | +| **ReClass.NET compatibility** | Load existing ReClass.NET native DLL plugins directly; optional .NET CLR hosting for managed plugins | ## MCP Integration -Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server starts automatically on launch and can be toggled from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code). A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`): +Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge` — the first reverse engineering tool with native AI/LLM integration. The server uses JSON-RPC 2.0 over named pipes and can be toggled from the Tools menu or auto-started on launch. + +**Available tools:** + +| Tool | Description | +|------|-------------| +| `projectState` | Read current tree structure, base address, tab state | +| `treeApply` | Apply structural command deltas to the node tree | +| `sourceSwitch` | Switch the active data source | +| `hexRead` | Read bytes at an address | +| `hexWrite` | Write bytes at an address | +| `statusSet` | Update the status bar text | +| `uiAction` | Trigger menu actions programmatically | +| `treeSearch` | Search nodes by name or type | +| `nodeHistory` | Query value change history for a node | + +**Notifications:** `notifyTreeChanged`, `notifyDataChanged` + +A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`): ```json { @@ -82,6 +161,20 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via ` } ``` +## Screenshots + +![macOS — project tree with kernel struct inspection](docs/README_PIC1.png) + +![Windows — VTable with value history popup](docs/README_PIC2.png) + +![Memory scanner](docs/README_PIC3.png) + +## Roadmap + +- [ ] iOS support +- [ ] Display RTTI information +- [ ] Expose UI functionality to plugins + ## Build ### Prerequisites @@ -92,7 +185,7 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via ` ### Quick Build -```/dev/null/commands.sh#L1-4 +```bash git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git cd Reclass .\scripts\build_qscintilla.ps1 @@ -103,13 +196,13 @@ The build script auto-detects your Qt install location. ### macOS Build -```/dev/null/commands.sh#L1-2 +```bash ./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package ``` If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`. -Note: macOS Gatekeeper may block unsigned apps. If the app won’t open, go to **System Settings → Privacy & Security** and click **Open Anyway**. +Note: macOS Gatekeeper may block unsigned apps. If the app won't open, go to **System Settings > Privacy & Security** and click **Open Anyway**. ### Manual Build (MinGW) @@ -132,6 +225,8 @@ The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects ctest --test-dir build --output-on-failure ``` +30 tests covering composition, serialization, undo/redo, import/export, provider switching, type visibility, validation, scanning, and rendering. + ## Alternatives - [ReClass.NET](https://github.com/ReClassNET/ReClass.NET) diff --git a/src/compose.cpp b/src/compose.cpp index c42f3ad..6318e63 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -24,6 +24,8 @@ struct ComposeState { int offsetHexDigits = 8; // hex digit tier for offset margin bool baseEmitted = false; // only first root struct shows base address bool compactColumns = false; // compact column mode: cap type width, overflow long types + bool treeLines = false; // draw Unicode tree connectors in indentation + QVector siblingStack; // per-depth: true = more siblings follow at this level uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target // Precomputed for O(1) lookups @@ -41,6 +43,15 @@ struct ComposeState { return scopeNameW.value(scopeId, nameW); } + // Set sibling-continuation flag for children at the given depth. + // childDepth is the depth of the children being iterated. + void setTreeSibling(int childDepth, bool hasMoreSiblings) { + if (!treeLines) return; + int d = childDepth - 1; + while (siblingStack.size() <= d) siblingStack.append(false); + siblingStack[d] = hasMoreSiblings; + } + void emitLine(const QString& lineText, LineMeta lm) { if (currentLine > 0) text += '\n'; // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other @@ -52,7 +63,29 @@ struct ComposeState { text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE "); else text += QStringLiteral(" "); - text += lineText; + + // Replace leading indent spaces with Unicode tree connectors + if (treeLines && lm.depth > 0) { + QString treeIndent; + int D = lm.depth; + bool isFooter = (lm.lineKind == LineKind::Footer); + for (int d = 0; d < D; d++) { + bool active = (d < siblingStack.size() && siblingStack[d]); + if (isFooter || d < D - 1) { + // Ancestor continuation or footer's own level + treeIndent += active ? QStringLiteral("\u2502 ") + : QStringLiteral(" "); + } else { + // This node's own connector (non-footer only) + treeIndent += active ? QStringLiteral("\u251C\u2500 ") + : QStringLiteral("\u2514\u2500 "); + } + } + text += treeIndent + lineText.mid(D * 3); + } else { + text += lineText; + } + meta.append(lm); currentLine++; } @@ -305,6 +338,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, }); for (int oi = 0; oi < order.size(); oi++) { + state.setTreeSibling(childDepth, oi < order.size() - 1); int mi = order[oi]; const auto& m = node.enumMembers[mi]; LineMeta lm; @@ -353,6 +387,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, maxNameLen = qMax(maxNameLen, (int)m.name.size()); for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) { + state.setTreeSibling(childDepth, mi < node.bitfieldMembers.size() - 1); const auto& m = node.bitfieldMembers[mi]; uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind, m.bitOffset, m.bitWidth); @@ -415,6 +450,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, int eTW = state.effectiveTypeW(node.id); int eNW = state.effectiveNameW(node.id); for (int i = 0; i < node.arrayLen; i++) { + state.setTreeSibling(childDepth, i < node.arrayLen - 1); uint64_t elemAddr = absAddr + i * elemSize; // Type override: "float[0]", "uint32_t[1]", etc. @@ -460,6 +496,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, int elemSize = tree.structSpan(node.refId, &state.childMap); if (elemSize <= 0) elemSize = 1; for (int i = 0; i < node.arrayLen; i++) { + state.setTreeSibling(childDepth, i < node.arrayLen - 1); uint64_t elemBase = absAddr + (uint64_t)i * elemSize; // Use base offset that maps refStruct's children to the right provider address composeParent(state, tree, prov, refIdx, childDepth, elemBase, node.refId, @@ -476,7 +513,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, const QVector& refChildren = childIndices(state, node.refId); // Use the referenced struct's scope widths (children come from there) uint64_t refScopeId = node.refId; - for (int childIdx : refChildren) { + for (int rci = 0; rci < refChildren.size(); rci++) { + int childIdx = refChildren[rci]; + state.setTreeSibling(childDepth, rci < refChildren.size() - 1); const Node& child = tree.nodes[childIdx]; // Self-referential child → show as collapsed struct (non-expandable) if (state.visiting.contains(child.id)) { @@ -514,7 +553,13 @@ void composeParent(ComposeState& state, const NodeTree& tree, // For arrays, render children as condensed (no header/footer for struct elements) bool childrenAreArrayElements = (node.kind == NodeKind::Array); int elementIdx = 0; - for (int childIdx : regular) { + for (int ri = 0; ri < regular.size(); ri++) { + int childIdx = regular[ri]; + // A regular child has more siblings if there are more regular children + // or if static fields follow after all regular children + bool hasMore = (ri < regular.size() - 1) + || (!staticIdxs.isEmpty() && !node.collapsed); + state.setTreeSibling(childDepth, hasMore); // Pass this container's id as the scope for children (for per-scope widths) // For array elements, also pass the element index for [N] separator composeNode(state, tree, prov, childIdx, childDepth, base, rootId, @@ -569,7 +614,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, auto cbs = makeResolver(absAddr); - for (int si : staticIdxs) { + for (int sii = 0; sii < staticIdxs.size(); sii++) { + int si = staticIdxs[sii]; + state.setTreeSibling(childDepth, sii < staticIdxs.size() - 1); const Node& sf = tree.nodes[si]; // Evaluate expression → absolute address @@ -639,8 +686,18 @@ void composeParent(ComposeState& state, const NodeTree& tree, // ── Body + children (only when expanded) ── if (!isCollapsed) { + // Determine if struct children follow the body line + bool hasStructKids = exprOk + && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array); + const QVector staticKids = hasStructKids + ? childIndices(state, sf.id) : QVector(); + hasStructKids = hasStructKids && !staticKids.isEmpty(); + // Body line: " return → 0xADDR" { + // Body has more siblings if struct children follow + state.setTreeSibling(childDepth + 1, hasStructKids); + QString bodyLine; if (!sf.offsetExpr.isEmpty()) { if (exprOk) @@ -676,10 +733,10 @@ void composeParent(ComposeState& state, const NodeTree& tree, } // If struct/array, compose children at evaluated address - if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) { - const QVector& staticKids = childIndices(state, sf.id); - for (int sci : staticKids) { - composeNode(state, tree, prov, sci, childDepth + 1, + if (hasStructKids) { + for (int ski = 0; ski < staticKids.size(); ski++) { + state.setTreeSibling(childDepth + 1, ski < staticKids.size() - 1); + composeNode(state, tree, prov, staticKids[ski], childDepth + 1, staticAddr, sf.id, false, sf.id); } } @@ -818,8 +875,9 @@ void composeNode(ComposeState& state, const NodeTree& tree, // Render materialized children at the pointer target address. // These are real tree nodes with independent state — use rootId // so resolveAddr computes offsets relative to the pointer target. - for (int childIdx : ptrChildren) { - composeNode(state, tree, childProv, childIdx, depth + 1, + for (int pci = 0; pci < ptrChildren.size(); pci++) { + state.setTreeSibling(depth + 1, pci < ptrChildren.size() - 1); + composeNode(state, tree, childProv, ptrChildren[pci], depth + 1, pBase, node.id, false, node.id); } } else { @@ -878,9 +936,10 @@ void composeNode(ComposeState& state, const NodeTree& tree, } // anonymous namespace ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId, - bool compactColumns) { + bool compactColumns, bool treeLines) { ComposeState state; state.compactColumns = compactColumns; + state.treeLines = treeLines; // Precompute parent→children map for (int i = 0; i < tree.nodes.size(); i++) @@ -1026,7 +1085,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR composeNode(state, tree, prov, idx, 0); } - return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress} }; + return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress, treeLines} }; } QSet NodeTree::normalizePreferAncestors(const QSet& ids) const { diff --git a/src/controller.cpp b/src/controller.cpp index 53cf2a6..da7bcde 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -72,8 +72,9 @@ RcxDocument::RcxDocument(QObject* parent) }); } -ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const { - return rcx::compose(tree, *provider, viewRootId, compactColumns); +ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns, + bool treeLines) const { + return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines); } bool RcxDocument::save(const QString& path) { @@ -319,7 +320,7 @@ void RcxController::connectEditor(RcxEditor* editor) { // Regular type change bool ok; NodeKind k = kindFromTypeName(text, &ok); - if (ok) { + if (ok && k != NodeKind::Struct && k != NodeKind::Array) { changeNodeKind(nodeIdx, k); } else if (nodeIdx < m_doc->tree.nodes.size()) { // Check if it's a defined struct type name @@ -546,6 +547,7 @@ void RcxController::resetChangeTracking() { m_changedOffsets.clear(); m_valueHistory.clear(); m_prevPages.clear(); + m_valueTrackCooldown = 5; // suppress tracking for ~1s for (auto& lm : m_lastResult.meta) lm.heatLevel = 0; } @@ -556,9 +558,9 @@ void RcxController::refresh() { // Compose against snapshot provider if active, otherwise real provider if (m_snapshotProv) - m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns); + m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines); else - m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns); + m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines); s_composeDoc = nullptr; @@ -602,7 +604,8 @@ void RcxController::refresh() { else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive()) prov = m_doc->provider.get(); - if (m_trackValues && prov) { + if (m_valueTrackCooldown > 0) --m_valueTrackCooldown; + if (m_trackValues && prov && m_valueTrackCooldown <= 0) { for (auto& lm : m_lastResult.meta) { if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue; if (isSyntheticLine(lm) || lm.isContinuation) continue; @@ -1710,6 +1713,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, m_refreshGen++; // discard in-flight async reads m_prevPages.clear(); // clean baseline for next read cycle m_changedOffsets.clear(); // no phantom change indicators + m_valueTrackCooldown = 5; // suppress tracking for ~1s refresh(); for (auto* editor : m_editors) editor->dismissHistoryPopup(); @@ -1935,6 +1939,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, m_refreshGen++; // discard in-flight async reads m_prevPages.clear(); // clean baseline for next read cycle m_changedOffsets.clear(); // no phantom change indicators + m_valueTrackCooldown = 5; // suppress tracking for ~1s refresh(); for (auto* editor : m_editors) editor->dismissHistoryPopup(); @@ -2608,7 +2613,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, break; case TypePopupMode::FieldType: { - addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false); + addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true); bool isPtr = node && (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64); bool isTypedPtr = isPtr && node->refId != 0; @@ -3181,6 +3186,11 @@ void RcxController::setCompactColumns(bool v) { refresh(); } +void RcxController::setTreeLines(bool v) { + m_treeLines = v; + refresh(); +} + void RcxController::setupAutoRefresh() { int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt(); m_refreshTimer = new QTimer(this); diff --git a/src/controller.h b/src/controller.h index 71a80bc..a22fd02 100644 --- a/src/controller.h +++ b/src/controller.h @@ -40,7 +40,8 @@ public: return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"); } - ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const; + ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false, + bool treeLines = false) const; bool save(const QString& path); bool load(const QString& path); void loadData(const QString& binaryPath); @@ -128,6 +129,7 @@ public: void setEditorFont(const QString& fontName); void setRefreshInterval(int ms); void setCompactColumns(bool v); + void setTreeLines(bool v); void resetProvider(); // MCP bridge accessors @@ -151,6 +153,7 @@ public: // Test accessors const QHash& valueHistory() const { return m_valueHistory; } const ComposeResult& lastResult() const { return m_lastResult; } + int dataExtent() const { return computeDataExtent(); } signals: void nodeSelected(int nodeIdx); @@ -164,6 +167,7 @@ private: int m_anchorLine = -1; bool m_suppressRefresh = false; bool m_compactColumns = false; + bool m_treeLines = false; uint64_t m_viewRootId = 0; // ── Saved sources for quick-switch ── @@ -183,6 +187,7 @@ private: QSet m_changedOffsets; QHash m_valueHistory; bool m_trackValues = true; + int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear uint64_t m_refreshGen = 0; uint64_t m_readGen = 0; bool m_readInFlight = false; diff --git a/src/core.h b/src/core.h index fc3c3c8..af50a16 100644 --- a/src/core.h +++ b/src/core.h @@ -86,8 +86,8 @@ inline constexpr KindMeta kKindMeta[] = { {NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector}, {NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector}, {NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None}, - {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String}, - {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String}, + {NodeKind::UTF8, "UTF8", "str", 1, 1, 1, KF_String}, + {NodeKind::UTF16, "UTF16", "wstr", 2, 1, 2, KF_String}, {NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container}, {NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container}, }; @@ -153,14 +153,11 @@ inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) { return true; } -inline QStringList allTypeNamesForUI(bool stripBrackets = false) { +inline QStringList allTypeNamesForUI(bool /*stripBrackets*/ = false) { QStringList out; out.reserve(std::size(kKindMeta)); - for (const auto& m : kKindMeta) { - QString t = QString::fromLatin1(m.typeName); - if (stripBrackets) t.remove(QStringLiteral("[]")); - out << t; - } + for (const auto& m : kKindMeta) + out << QString::fromLatin1(m.typeName); out.sort(Qt::CaseInsensitive); out.removeDuplicates(); return out; @@ -636,6 +633,7 @@ struct LayoutInfo { int nameW = 22; // Effective name column width (default = kColName) int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16) uint64_t baseAddress = 0; // Base address for relative offset computation + bool treeLines = false; // Whether tree line connectors are embedded in the text }; // ── ComposeResult ── @@ -1033,6 +1031,6 @@ namespace fmt { // ── Compose function forward declaration ── ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0, - bool compactColumns = false); + bool compactColumns = false, bool treeLines = false); } // namespace rcx diff --git a/src/editor.cpp b/src/editor.cpp index 6aa472b..405cf6b 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -156,7 +156,7 @@ public: adjustSize(); } - void showAt(const QPoint& globalPos) { + void showAt(const QPoint& globalPos, int lineHeight = 0) { QSize sz = sizeHint(); QRect screen = QApplication::screenAt(globalPos) ? QApplication::screenAt(globalPos)->availableGeometry() @@ -164,7 +164,7 @@ public: int x = qMin(globalPos.x(), screen.right() - sz.width()); int y = globalPos.y(); if (y + sz.height() > screen.bottom()) - y = globalPos.y() - sz.height() - 4; + y = globalPos.y() - sz.height() - lineHeight - 4; move(x, y); if (!isVisible()) show(); } @@ -257,7 +257,7 @@ public: adjustSize(); } - void showAt(const QPoint& globalPos) { + void showAt(const QPoint& globalPos, int lineHeight = 0) { QSize sz = sizeHint(); QRect screen = QApplication::screenAt(globalPos) ? QApplication::screenAt(globalPos)->availableGeometry() @@ -265,7 +265,7 @@ public: int x = qMin(globalPos.x(), screen.right() - sz.width()); int y = globalPos.y(); if (y + sz.height() > screen.bottom()) - y = globalPos.y() - sz.height() - 4; + y = globalPos.y() - sz.height() - lineHeight - 4; move(x, y); if (!isVisible()) show(); } @@ -354,7 +354,7 @@ public: adjustSize(); } - void showAt(const QPoint& globalPos) { + void showAt(const QPoint& globalPos, int lineHeight = 0) { QSize sz = sizeHint(); QRect screen = QApplication::screenAt(globalPos) ? QApplication::screenAt(globalPos)->availableGeometry() @@ -362,7 +362,7 @@ public: int x = qMin(globalPos.x(), screen.right() - sz.width()); int y = globalPos.y(); if (y + sz.height() > screen.bottom()) - y = globalPos.y() - sz.height() - 4; + y = globalPos.y() - sz.height() - lineHeight - 4; move(x, y); if (!isVisible()) show(); } @@ -866,7 +866,7 @@ void RcxEditor::applyTheme(const Theme& theme) { m_sci->setMarkerBackgroundColor(theme.background, M_CYCLE); m_sci->setMarkerForegroundColor(theme.background, M_CYCLE); m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR); - m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR); + m_sci->setMarkerForegroundColor(theme.text, M_ERR); m_sci->setMarkerBackgroundColor(theme.background, M_STRUCT_BG); m_sci->setMarkerForegroundColor(theme.text, M_STRUCT_BG); m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER); @@ -1061,6 +1061,11 @@ void RcxEditor::reformatMargins() { } // ── Pass 2: inline local offsets in the text indent area ── + // Skip when tree lines are active — the compose step already placed + // Unicode tree connectors in the indent area; overwriting with spaces + // or offsets would destroy them. + if (m_layout.treeLines) + return; m_sci->setReadOnly(false); for (int i = 0; i < m_meta.size(); i++) { const auto& lm = m_meta[i]; @@ -2204,11 +2209,25 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { m_lastHoverPos = static_cast(event)->pos(); m_hoverInside = true; } else if (event->type() == QEvent::Leave) { - m_hoverInside = false; - if (!m_editState.active) { - m_hoveredNodeId = 0; - m_hoveredLine = -1; - applyHoverHighlight(); + // Don't dismiss if cursor moved onto one of our own popups + QPoint globalCursor = QCursor::pos(); + bool onPopup = false; + if (m_historyPopup && m_historyPopup->isVisible() + && m_historyPopup->geometry().contains(globalCursor)) + onPopup = true; + if (m_disasmPopup && m_disasmPopup->isVisible() + && m_disasmPopup->geometry().contains(globalCursor)) + onPopup = true; + if (m_structPreviewPopup && m_structPreviewPopup->isVisible() + && m_structPreviewPopup->geometry().contains(globalCursor)) + onPopup = true; + if (!onPopup) { + m_hoverInside = false; + if (!m_editState.active) { + m_hoveredNodeId = 0; + m_hoveredLine = -1; + applyHoverHighlight(); + } } } else if (event->type() == QEvent::Wheel) { m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos()); @@ -2992,7 +3011,7 @@ void RcxEditor::applyHoverCursor() { int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (unsigned long)m_editState.line); QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh)); - popup->showAt(anchor); + popup->showAt(anchor, lh); showPopup = true; } } @@ -3147,7 +3166,7 @@ void RcxEditor::applyHoverCursor() { int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (unsigned long)h.line); QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh)); - popup->showAt(anchor); + popup->showAt(anchor, lh); showPopup = true; } } @@ -3240,7 +3259,7 @@ void RcxEditor::applyHoverCursor() { (unsigned long)h.line); QPoint anchor = m_sci->viewport()->mapToGlobal( QPoint(px, py + lh)); - popup->showAt(anchor); + popup->showAt(anchor, lh); showDisasm = true; // Dismiss value history popup to avoid fighting if (m_historyPopup && m_historyPopup->isVisible()) @@ -3307,7 +3326,7 @@ void RcxEditor::applyHoverCursor() { (unsigned long)h.line); QPoint anchor = m_sci->viewport()->mapToGlobal( QPoint(px, py + lh)); - popup->showAt(anchor); + popup->showAt(anchor, lh); showPreview = true; if (m_historyPopup && m_historyPopup->isVisible()) static_cast(m_historyPopup)->dismiss(); diff --git a/src/examples/Demo.rcx b/src/examples/Demo.rcx new file mode 100644 index 0000000..b361f97 --- /dev/null +++ b/src/examples/Demo.rcx @@ -0,0 +1,143 @@ +{ + "baseAddress": "0", + "nextId": "20", + "nodes": [ + { + "id": "1", + "kind": "Struct", + "name": "player", + "structTypeName": "PlayerEntity", + "classKeyword": "class", + "parentId": "0", + "offset": 0, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "2", + "kind": "Pointer64", + "name": "__vptr", + "parentId": "1", + "offset": 0, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "3", + "kind": "Int32", + "name": "health", + "parentId": "1", + "offset": 8, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "4", + "kind": "Int32", + "name": "armor", + "parentId": "1", + "offset": 12, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "5", + "kind": "Float", + "name": "pos_x", + "parentId": "1", + "offset": 16, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "6", + "kind": "Float", + "name": "pos_y", + "parentId": "1", + "offset": 20, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "7", + "kind": "Float", + "name": "pos_z", + "parentId": "1", + "offset": 24, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "8", + "kind": "Hex32", + "name": "pad_1C", + "parentId": "1", + "offset": 28, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "9", + "kind": "Pointer64", + "name": "name", + "parentId": "1", + "offset": 32, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64, + "ptrDepth": 1 + }, + { + "id": "10", + "kind": "UInt64", + "name": "flags", + "parentId": "1", + "offset": 40, + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + }, + { + "id": "11", + "kind": "Hex64", + "name": "static_field", + "parentId": "1", + "offset": 0, + "isStatic": true, + "offsetExpr": "base + pos_x", + "collapsed": false, + "refId": "0", + "elementKind": "UInt8", + "arrayLen": 1, + "strLen": 64 + } + ] +} diff --git a/src/examples/t6zm.rcx b/src/examples/t6zm.rcx index 9009832..024e765 100644 --- a/src/examples/t6zm.rcx +++ b/src/examples/t6zm.rcx @@ -1,6 +1,6 @@ { "baseAddress": "2346aa0", - "nextId": "4935", + "nextId": "4957", "nodes": [ { "arrayLen": 1, @@ -14,30 +14,6 @@ "refId": "0", "strLen": 64 }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "85", - "kind": "UInt32", - "name": "accountId", - "offset": 0, - "parentId": "84", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "86", - "kind": "UInt32", - "name": "universe", - "offset": 4, - "parentId": "84", - "refId": "0", - "strLen": 64 - }, { "arrayLen": 1, "classKeyword": "class", @@ -52,19 +28,6 @@ "strLen": 64, "structTypeName": "UnkGlobal" }, - { - "arrayLen": 18, - "collapsed": true, - "elementKind": "Struct", - "id": "110", - "kind": "Array", - "name": "players", - "offset": 0, - "parentId": "109", - "refId": "87", - "strLen": 64, - "structTypeName": "PlayerState" - }, { "arrayLen": 1, "collapsed": false, @@ -104,6 +67,56 @@ "strLen": 64, "structTypeName": "clientConnection_t" }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4937", + "kind": "Struct", + "name": "weaponEntry_t", + "offset": 0, + "parentId": "0", + "refId": "0", + "strLen": 64, + "structTypeName": "weaponEntry_t" + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "85", + "kind": "UInt32", + "name": "accountId", + "offset": 0, + "parentId": "84", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "86", + "kind": "UInt32", + "name": "universe", + "offset": 4, + "parentId": "84", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 18, + "collapsed": true, + "elementKind": "Struct", + "id": "110", + "kind": "Array", + "name": "players", + "offset": 0, + "parentId": "109", + "refId": "87", + "strLen": 64, + "structTypeName": "PlayerState" + }, { "arrayLen": 1, "collapsed": false, @@ -816,9 +829,9 @@ "arrayLen": 1, "collapsed": false, "elementKind": "UInt8", - "id": "3124", - "kind": "Hex64", - "name": "", + "id": "4948", + "kind": "UInt16", + "name": "field_158_a", "offset": 344, "parentId": "3059", "refId": "0", @@ -828,10 +841,46 @@ "arrayLen": 1, "collapsed": false, "elementKind": "UInt8", - "id": "3125", + "id": "4949", + "kind": "UInt16", + "name": "field_158_b", + "offset": 346, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 4, + "collapsed": true, + "elementKind": "Int32", + "id": "4950", + "kind": "Array", + "name": "field_15C", + "offset": 348, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 4, + "collapsed": true, + "elementKind": "Int32", + "id": "4951", + "kind": "Array", + "name": "field_16C", + "offset": 364, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4952", "kind": "Hex64", "name": "", - "offset": 352, + "offset": 380, "parentId": "3059", "refId": "0", "strLen": 64 @@ -840,34 +889,10 @@ "arrayLen": 1, "collapsed": false, "elementKind": "UInt8", - "id": "3126", - "kind": "Hex16", - "name": "", - "offset": 360, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3127", - "kind": "UInt8", - "name": "field_16A", - "offset": 362, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3128", + "id": "4953", "kind": "Hex64", "name": "", - "offset": 363, + "offset": 388, "parentId": "3059", "refId": "0", "strLen": 64 @@ -876,46 +901,10 @@ "arrayLen": 1, "collapsed": false, "elementKind": "UInt8", - "id": "3129", + "id": "4954", "kind": "Hex64", "name": "", - "offset": 371, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3130", - "kind": "Hex64", - "name": "", - "offset": 379, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3131", - "kind": "Hex64", - "name": "", - "offset": 387, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3132", - "kind": "Hex64", - "name": "", - "offset": 395, + "offset": 396, "parentId": "3059", "refId": "0", "strLen": 64 @@ -996,14 +985,26 @@ "arrayLen": 1, "collapsed": false, "elementKind": "UInt8", - "id": "3139", - "kind": "Hex64", + "id": "4955", + "kind": "Hex32", "name": "", "offset": 436, "parentId": "3059", "refId": "0", "strLen": 64 }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4956", + "kind": "Int32", + "name": "currentWeaponId", + "offset": 440, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, { "arrayLen": 1, "collapsed": false, @@ -1185,725 +1186,42 @@ "strLen": 64 }, { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3155", - "kind": "Int32", - "name": "weapon0_id", + "arrayLen": 15, + "collapsed": true, + "elementKind": "Struct", + "id": "4945", + "kind": "Array", + "name": "weapons", "offset": 584, "parentId": "3059", - "refId": "0", - "strLen": 64 + "refId": "4937", + "strLen": 64, + "structTypeName": "weaponEntry_t" }, { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3156", - "kind": "Int32", - "name": "weapon0_f1", - "offset": 588, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3157", - "kind": "Int32", - "name": "weapon0_f2", - "offset": 592, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3158", - "kind": "Int32", - "name": "weapon0_f3", - "offset": 596, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3159", - "kind": "Int32", - "name": "weapon0_f4", - "offset": 600, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3160", - "kind": "Int32", - "name": "weapon0_f5", - "offset": 604, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3161", - "kind": "Int32", - "name": "weapon0_flags", - "offset": 608, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3162", - "kind": "Int32", - "name": "weapon1_id", - "offset": 612, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3163", - "kind": "Int32", - "name": "weapon1_f1", - "offset": 616, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3164", - "kind": "Int32", - "name": "weapon1_f2", - "offset": 620, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3165", - "kind": "Int32", - "name": "weapon1_f3", - "offset": 624, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3166", - "kind": "Int32", - "name": "weapon1_f4", - "offset": 628, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3167", - "kind": "Int32", - "name": "weapon1_f5", - "offset": 632, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3168", - "kind": "Int32", - "name": "weapon1_flags", - "offset": 636, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3169", - "kind": "Hex64", - "name": "gap_0280", - "offset": 640, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3170", - "kind": "Hex64", - "name": "", - "offset": 648, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3171", - "kind": "Hex64", - "name": "", - "offset": 656, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3172", - "kind": "Hex64", - "name": "", - "offset": 664, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3173", - "kind": "Hex64", - "name": "", - "offset": 672, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3174", - "kind": "Hex64", - "name": "", - "offset": 680, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3175", - "kind": "Hex64", - "name": "", - "offset": 688, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3176", - "kind": "Hex64", - "name": "", - "offset": 696, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3177", - "kind": "Hex64", - "name": "", - "offset": 940, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3178", - "kind": "Hex64", - "name": "", - "offset": 948, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3179", - "kind": "Hex64", - "name": "", - "offset": 956, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3180", - "kind": "Hex64", - "name": "", - "offset": 964, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3181", - "kind": "Hex64", - "name": "", - "offset": 972, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3182", - "kind": "Hex64", - "name": "", - "offset": 980, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3183", - "kind": "Hex64", - "name": "", - "offset": 988, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3184", - "kind": "Hex64", - "name": "", - "offset": 996, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3185", - "kind": "Int32", - "name": "clipAmmo_0", + "arrayLen": 15, + "collapsed": true, + "elementKind": "Int32", + "id": "4946", + "kind": "Array", + "name": "clipAmmo", "offset": 1004, "parentId": "3059", "refId": "0", "strLen": 64 }, { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3186", - "kind": "Int32", - "name": "clipAmmo_1", - "offset": 1008, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3187", - "kind": "Int32", - "name": "clipAmmo_2", - "offset": 1012, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3188", - "kind": "Int32", - "name": "clipAmmo_3", - "offset": 1016, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3189", - "kind": "Int32", - "name": "clipAmmo_4", - "offset": 1020, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3190", - "kind": "Int32", - "name": "clipAmmo_5", - "offset": 1024, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3191", - "kind": "Int32", - "name": "clipAmmo_6", - "offset": 1028, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3192", - "kind": "Int32", - "name": "clipAmmo_7", - "offset": 1032, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3193", - "kind": "Int32", - "name": "clipAmmo_8", - "offset": 1036, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3194", - "kind": "Int32", - "name": "clipAmmo_9", - "offset": 1040, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3195", - "kind": "Int32", - "name": "clipAmmo_10", - "offset": 1044, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3196", - "kind": "Int32", - "name": "clipAmmo_11", - "offset": 1048, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3197", - "kind": "Int32", - "name": "clipAmmo_12", - "offset": 1052, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3198", - "kind": "Int32", - "name": "clipAmmo_13", - "offset": 1056, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3199", - "kind": "Int32", - "name": "clipAmmo_14", - "offset": 1060, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3200", - "kind": "Int32", - "name": "stockAmmo_0", + "arrayLen": 15, + "collapsed": true, + "elementKind": "Int32", + "id": "4947", + "kind": "Array", + "name": "stockAmmo", "offset": 1064, "parentId": "3059", "refId": "0", "strLen": 64 }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3201", - "kind": "Int32", - "name": "stockAmmo_1", - "offset": 1068, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3202", - "kind": "Int32", - "name": "stockAmmo_2", - "offset": 1072, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3203", - "kind": "Int32", - "name": "stockAmmo_3", - "offset": 1076, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3204", - "kind": "Int32", - "name": "stockAmmo_4", - "offset": 1080, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3205", - "kind": "Int32", - "name": "stockAmmo_5", - "offset": 1084, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3206", - "kind": "Int32", - "name": "stockAmmo_6", - "offset": 1088, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3207", - "kind": "Int32", - "name": "stockAmmo_7", - "offset": 1092, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3208", - "kind": "Int32", - "name": "stockAmmo_8", - "offset": 1096, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3209", - "kind": "Int32", - "name": "stockAmmo_9", - "offset": 1100, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3210", - "kind": "Int32", - "name": "stockAmmo_10", - "offset": 1104, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3211", - "kind": "Int32", - "name": "stockAmmo_11", - "offset": 1108, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3212", - "kind": "Int32", - "name": "stockAmmo_12", - "offset": 1112, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3213", - "kind": "Int32", - "name": "stockAmmo_13", - "offset": 1116, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3214", - "kind": "Int32", - "name": "stockAmmo_14", - "offset": 1120, - "parentId": "3059", - "refId": "0", - "strLen": 64 - }, { "arrayLen": 1, "collapsed": false, @@ -2234,7 +1552,7 @@ "elementKind": "UInt8", "id": "3242", "kind": "UInt32", - "name": "perks", + "name": "perks_ps_0", "offset": 1352, "parentId": "3059", "refId": "0", @@ -2245,8 +1563,8 @@ "collapsed": false, "elementKind": "UInt8", "id": "3243", - "kind": "Float", - "name": "weaponMod1", + "kind": "UInt32", + "name": "perks_ps_1", "offset": 1356, "parentId": "3059", "refId": "0", @@ -2257,8 +1575,8 @@ "collapsed": false, "elementKind": "UInt8", "id": "3244", - "kind": "Float", - "name": "weaponMod2", + "kind": "Hex32", + "name": "", "offset": 1360, "parentId": "3059", "refId": "0", @@ -2281,8 +1599,8 @@ "collapsed": false, "elementKind": "UInt8", "id": "3246", - "kind": "Float", - "name": "weaponFloat1", + "kind": "Int32", + "name": "field_558", "offset": 1368, "parentId": "3059", "refId": "0", @@ -2353,13 +1671,25 @@ "collapsed": false, "elementKind": "UInt8", "id": "3252", - "kind": "Hex64", - "name": "", + "kind": "UInt32", + "name": "speedOverrideFlag", "offset": 1400, "parentId": "3059", "refId": "0", "strLen": 64 }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4935", + "kind": "Float", + "name": "speedOverrideValue", + "offset": 1404, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, { "arrayLen": 1, "collapsed": false, @@ -3577,13 +2907,25 @@ "collapsed": false, "elementKind": "UInt8", "id": "3554", - "kind": "Hex64", - "name": "", + "kind": "UInt32", + "name": "perks_server_0", "offset": 21908, "parentId": "3059", "refId": "0", "strLen": 64 }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4936", + "kind": "UInt32", + "name": "perks_server_1", + "offset": 21912, + "parentId": "3059", + "refId": "0", + "strLen": 64 + }, { "arrayLen": 1, "collapsed": false, @@ -5048,1566 +4390,6 @@ "refId": "0", "strLen": 64 }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3889", - "kind": "Hex32", - "name": "", - "offset": 0, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3890", - "kind": "UInt8", - "name": "netadr_type", - "offset": 4, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3891", - "kind": "UInt8", - "name": "netadr_b5", - "offset": 5, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3892", - "kind": "UInt8", - "name": "netadr_b6", - "offset": 6, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3893", - "kind": "UInt8", - "name": "netadr_b7", - "offset": 7, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3894", - "kind": "Int32", - "name": "lastMsgTime1", - "offset": 8, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3895", - "kind": "Hex32", - "name": "", - "offset": 12, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3896", - "kind": "Int32", - "name": "field_10", - "offset": 16, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3897", - "kind": "Hex64", - "name": "", - "offset": 20, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3898", - "kind": "Hex64", - "name": "", - "offset": 28, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3899", - "kind": "Int32", - "name": "lastMsgTime2", - "offset": 36, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3900", - "kind": "UInt32", - "name": "serverField_A", - "offset": 40, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3901", - "kind": "Hex64", - "name": "serverMsg_0", - "offset": 44, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3902", - "kind": "Hex64", - "name": "serverMsg_8", - "offset": 52, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3903", - "kind": "Hex64", - "name": "serverMsg_16", - "offset": 60, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3904", - "kind": "Hex64", - "name": "serverMsg_24", - "offset": 68, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3905", - "kind": "Hex64", - "name": "gap_004C", - "offset": 76, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3906", - "kind": "Hex64", - "name": "", - "offset": 84, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3907", - "kind": "Hex64", - "name": "", - "offset": 92, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3908", - "kind": "Hex64", - "name": "", - "offset": 100, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3909", - "kind": "Hex64", - "name": "", - "offset": 108, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3910", - "kind": "Hex64", - "name": "", - "offset": 116, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3911", - "kind": "Hex64", - "name": "", - "offset": 124, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3912", - "kind": "Hex64", - "name": "", - "offset": 132, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3913", - "kind": "Hex64", - "name": "", - "offset": 4240, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3914", - "kind": "Hex64", - "name": "", - "offset": 4248, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3915", - "kind": "Hex64", - "name": "", - "offset": 4256, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3916", - "kind": "Hex64", - "name": "", - "offset": 4264, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3917", - "kind": "Hex64", - "name": "", - "offset": 4272, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3918", - "kind": "Hex64", - "name": "", - "offset": 4280, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3919", - "kind": "Hex64", - "name": "", - "offset": 4288, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3920", - "kind": "Hex64", - "name": "", - "offset": 4296, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3921", - "kind": "Int32", - "name": "counter_10D0", - "offset": 4304, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3922", - "kind": "Hex64", - "name": "gap_10D4", - "offset": 4308, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3923", - "kind": "Hex64", - "name": "", - "offset": 4316, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3924", - "kind": "Hex64", - "name": "", - "offset": 4324, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3925", - "kind": "Hex64", - "name": "", - "offset": 4332, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3926", - "kind": "Hex64", - "name": "", - "offset": 4340, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3927", - "kind": "Hex64", - "name": "", - "offset": 4348, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3928", - "kind": "Hex64", - "name": "", - "offset": 4356, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3929", - "kind": "Hex64", - "name": "", - "offset": 4364, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3930", - "kind": "Hex64", - "name": "", - "offset": 148236, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3931", - "kind": "Hex64", - "name": "", - "offset": 148244, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3932", - "kind": "Hex64", - "name": "", - "offset": 148252, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3933", - "kind": "Hex64", - "name": "", - "offset": 148260, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3934", - "kind": "Hex64", - "name": "", - "offset": 148268, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3935", - "kind": "Hex64", - "name": "", - "offset": 148276, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3936", - "kind": "Hex64", - "name": "", - "offset": 148284, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3937", - "kind": "Hex64", - "name": "", - "offset": 148292, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3938", - "kind": "UInt8", - "name": "fastRestart", - "offset": 148300, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3939", - "kind": "UInt8", - "name": "connByte1", - "offset": 148301, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3940", - "kind": "UInt8", - "name": "connByte2", - "offset": 148302, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3941", - "kind": "Hex8", - "name": "", - "offset": 148303, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3942", - "kind": "Hex64", - "name": "netchan_0", - "offset": 148304, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3943", - "kind": "Hex64", - "name": "netchan_8", - "offset": 148312, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3944", - "kind": "Hex64", - "name": "netchan_16", - "offset": 148320, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3945", - "kind": "Hex64", - "name": "netchan_24", - "offset": 148328, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3946", - "kind": "Hex32", - "name": "netchan_32", - "offset": 148336, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3947", - "kind": "Hex64", - "name": "gap_24374", - "offset": 148340, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3948", - "kind": "Hex64", - "name": "", - "offset": 148348, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3949", - "kind": "Hex64", - "name": "", - "offset": 148356, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3950", - "kind": "Hex64", - "name": "", - "offset": 148364, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3951", - "kind": "Hex64", - "name": "", - "offset": 148372, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3952", - "kind": "Hex64", - "name": "", - "offset": 148380, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3953", - "kind": "Hex64", - "name": "", - "offset": 148388, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3954", - "kind": "Hex64", - "name": "", - "offset": 148396, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3955", - "kind": "Hex64", - "name": "", - "offset": 149976, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3956", - "kind": "Hex64", - "name": "", - "offset": 149984, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3957", - "kind": "Hex64", - "name": "", - "offset": 149992, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3958", - "kind": "Hex64", - "name": "", - "offset": 150000, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3959", - "kind": "Hex64", - "name": "", - "offset": 150008, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3960", - "kind": "Hex64", - "name": "", - "offset": 150016, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3961", - "kind": "Hex64", - "name": "", - "offset": 150024, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3962", - "kind": "Hex64", - "name": "", - "offset": 150032, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3963", - "kind": "Hex64", - "name": "reliableCmds_0", - "offset": 150040, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3964", - "kind": "Hex64", - "name": "gap_24A20", - "offset": 150048, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3965", - "kind": "Hex64", - "name": "", - "offset": 150056, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3966", - "kind": "Hex64", - "name": "", - "offset": 150064, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3967", - "kind": "Hex64", - "name": "", - "offset": 150072, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3968", - "kind": "Hex64", - "name": "", - "offset": 150080, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3969", - "kind": "Hex64", - "name": "", - "offset": 150088, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3970", - "kind": "Hex64", - "name": "", - "offset": 150096, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3971", - "kind": "Hex64", - "name": "", - "offset": 150104, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3972", - "kind": "Hex64", - "name": "", - "offset": 152024, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3973", - "kind": "Hex64", - "name": "", - "offset": 152032, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3974", - "kind": "Hex64", - "name": "", - "offset": 152040, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3975", - "kind": "Hex64", - "name": "", - "offset": 152048, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3976", - "kind": "Hex64", - "name": "", - "offset": 152056, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3977", - "kind": "Hex64", - "name": "", - "offset": 152064, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3978", - "kind": "Hex64", - "name": "", - "offset": 152072, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3979", - "kind": "Hex64", - "name": "", - "offset": 152080, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3980", - "kind": "Hex64", - "name": "serverCmds_0", - "offset": 152088, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3981", - "kind": "Hex64", - "name": "gap_25220", - "offset": 152096, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3982", - "kind": "Hex64", - "name": "", - "offset": 152104, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3983", - "kind": "Hex64", - "name": "", - "offset": 152112, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3984", - "kind": "Hex64", - "name": "", - "offset": 152120, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3985", - "kind": "Hex64", - "name": "", - "offset": 152128, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3986", - "kind": "Hex64", - "name": "", - "offset": 152136, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3987", - "kind": "Hex64", - "name": "", - "offset": 152144, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3988", - "kind": "Hex64", - "name": "", - "offset": 152152, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3989", - "kind": "Hex64", - "name": "", - "offset": 219064, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3990", - "kind": "Hex64", - "name": "", - "offset": 219072, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3991", - "kind": "Hex64", - "name": "", - "offset": 219080, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3992", - "kind": "Hex64", - "name": "", - "offset": 219088, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3993", - "kind": "Hex64", - "name": "", - "offset": 219096, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3994", - "kind": "Hex64", - "name": "", - "offset": 219104, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3995", - "kind": "Hex64", - "name": "", - "offset": 219112, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3996", - "kind": "Hex64", - "name": "", - "offset": 219120, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3997", - "kind": "Int32", - "name": "statsSeq_lo", - "offset": 219128, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3998", - "kind": "Int32", - "name": "statsSeq_hi", - "offset": 219132, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "3999", - "kind": "Hex64", - "name": "statsMem_0", - "offset": 219136, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4000", - "kind": "Hex64", - "name": "statsMem_8", - "offset": 219144, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4001", - "kind": "Hex64", - "name": "statsMem_16", - "offset": 219152, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4002", - "kind": "Hex64", - "name": "statsMem_24", - "offset": 219160, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4003", - "kind": "Hex64", - "name": "gap_35820", - "offset": 219168, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4004", - "kind": "Hex64", - "name": "", - "offset": 219176, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4005", - "kind": "Hex64", - "name": "", - "offset": 219184, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4006", - "kind": "Hex64", - "name": "", - "offset": 219192, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4007", - "kind": "Hex64", - "name": "", - "offset": 219200, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4008", - "kind": "Hex64", - "name": "", - "offset": 219208, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4009", - "kind": "Hex64", - "name": "", - "offset": 219216, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4010", - "kind": "Hex64", - "name": "", - "offset": 219224, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4011", - "kind": "Hex64", - "name": "", - "offset": 219236, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4012", - "kind": "Hex64", - "name": "", - "offset": 219244, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4013", - "kind": "Hex64", - "name": "", - "offset": 219252, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4014", - "kind": "Hex64", - "name": "", - "offset": 219260, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4015", - "kind": "Hex64", - "name": "", - "offset": 219268, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4016", - "kind": "Hex64", - "name": "", - "offset": 219276, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4017", - "kind": "Hex64", - "name": "", - "offset": 219284, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, - { - "arrayLen": 1, - "collapsed": false, - "elementKind": "UInt8", - "id": "4018", - "kind": "Hex64", - "name": "", - "offset": 219292, - "parentId": "3063", - "refId": "0", - "strLen": 64 - }, { "arrayLen": 1, "collapsed": false, @@ -11323,7 +9105,1651 @@ "parentId": "3061", "refId": "0", "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3889", + "kind": "Hex32", + "name": "", + "offset": 0, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3890", + "kind": "UInt8", + "name": "netadr_type", + "offset": 4, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3891", + "kind": "UInt8", + "name": "netadr_b5", + "offset": 5, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3892", + "kind": "UInt8", + "name": "netadr_b6", + "offset": 6, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3893", + "kind": "UInt8", + "name": "netadr_b7", + "offset": 7, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3894", + "kind": "Int32", + "name": "lastMsgTime1", + "offset": 8, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3895", + "kind": "Hex32", + "name": "", + "offset": 12, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3896", + "kind": "Int32", + "name": "field_10", + "offset": 16, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3897", + "kind": "Hex64", + "name": "", + "offset": 20, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3898", + "kind": "Hex64", + "name": "", + "offset": 28, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3899", + "kind": "Int32", + "name": "lastMsgTime2", + "offset": 36, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3900", + "kind": "UInt32", + "name": "serverField_A", + "offset": 40, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3901", + "kind": "Hex64", + "name": "serverMsg_0", + "offset": 44, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3902", + "kind": "Hex64", + "name": "serverMsg_8", + "offset": 52, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3903", + "kind": "Hex64", + "name": "serverMsg_16", + "offset": 60, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3904", + "kind": "Hex64", + "name": "serverMsg_24", + "offset": 68, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3905", + "kind": "Hex64", + "name": "gap_004C", + "offset": 76, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3906", + "kind": "Hex64", + "name": "", + "offset": 84, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3907", + "kind": "Hex64", + "name": "", + "offset": 92, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3908", + "kind": "Hex64", + "name": "", + "offset": 100, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3909", + "kind": "Hex64", + "name": "", + "offset": 108, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3910", + "kind": "Hex64", + "name": "", + "offset": 116, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3911", + "kind": "Hex64", + "name": "", + "offset": 124, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3912", + "kind": "Hex64", + "name": "", + "offset": 132, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3913", + "kind": "Hex64", + "name": "", + "offset": 4240, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3914", + "kind": "Hex64", + "name": "", + "offset": 4248, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3915", + "kind": "Hex64", + "name": "", + "offset": 4256, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3916", + "kind": "Hex64", + "name": "", + "offset": 4264, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3917", + "kind": "Hex64", + "name": "", + "offset": 4272, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3918", + "kind": "Hex64", + "name": "", + "offset": 4280, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3919", + "kind": "Hex64", + "name": "", + "offset": 4288, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3920", + "kind": "Hex64", + "name": "", + "offset": 4296, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3921", + "kind": "Int32", + "name": "counter_10D0", + "offset": 4304, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3922", + "kind": "Hex64", + "name": "gap_10D4", + "offset": 4308, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3923", + "kind": "Hex64", + "name": "", + "offset": 4316, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3924", + "kind": "Hex64", + "name": "", + "offset": 4324, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3925", + "kind": "Hex64", + "name": "", + "offset": 4332, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3926", + "kind": "Hex64", + "name": "", + "offset": 4340, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3927", + "kind": "Hex64", + "name": "", + "offset": 4348, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3928", + "kind": "Hex64", + "name": "", + "offset": 4356, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3929", + "kind": "Hex64", + "name": "", + "offset": 4364, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3930", + "kind": "Hex64", + "name": "", + "offset": 148236, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3931", + "kind": "Hex64", + "name": "", + "offset": 148244, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3932", + "kind": "Hex64", + "name": "", + "offset": 148252, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3933", + "kind": "Hex64", + "name": "", + "offset": 148260, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3934", + "kind": "Hex64", + "name": "", + "offset": 148268, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3935", + "kind": "Hex64", + "name": "", + "offset": 148276, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3936", + "kind": "Hex64", + "name": "", + "offset": 148284, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3937", + "kind": "Hex64", + "name": "", + "offset": 148292, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3938", + "kind": "UInt8", + "name": "fastRestart", + "offset": 148300, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3939", + "kind": "UInt8", + "name": "connByte1", + "offset": 148301, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3940", + "kind": "UInt8", + "name": "connByte2", + "offset": 148302, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3941", + "kind": "Hex8", + "name": "", + "offset": 148303, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3942", + "kind": "Hex64", + "name": "netchan_0", + "offset": 148304, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3943", + "kind": "Hex64", + "name": "netchan_8", + "offset": 148312, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3944", + "kind": "Hex64", + "name": "netchan_16", + "offset": 148320, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3945", + "kind": "Hex64", + "name": "netchan_24", + "offset": 148328, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3946", + "kind": "Hex32", + "name": "netchan_32", + "offset": 148336, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3947", + "kind": "Hex64", + "name": "gap_24374", + "offset": 148340, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3948", + "kind": "Hex64", + "name": "", + "offset": 148348, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3949", + "kind": "Hex64", + "name": "", + "offset": 148356, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3950", + "kind": "Hex64", + "name": "", + "offset": 148364, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3951", + "kind": "Hex64", + "name": "", + "offset": 148372, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3952", + "kind": "Hex64", + "name": "", + "offset": 148380, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3953", + "kind": "Hex64", + "name": "", + "offset": 148388, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3954", + "kind": "Hex64", + "name": "", + "offset": 148396, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3955", + "kind": "Hex64", + "name": "", + "offset": 149976, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3956", + "kind": "Hex64", + "name": "", + "offset": 149984, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3957", + "kind": "Hex64", + "name": "", + "offset": 149992, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3958", + "kind": "Hex64", + "name": "", + "offset": 150000, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3959", + "kind": "Hex64", + "name": "", + "offset": 150008, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3960", + "kind": "Hex64", + "name": "", + "offset": 150016, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3961", + "kind": "Hex64", + "name": "", + "offset": 150024, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3962", + "kind": "Hex64", + "name": "", + "offset": 150032, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3963", + "kind": "Hex64", + "name": "reliableCmds_0", + "offset": 150040, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3964", + "kind": "Hex64", + "name": "gap_24A20", + "offset": 150048, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3965", + "kind": "Hex64", + "name": "", + "offset": 150056, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3966", + "kind": "Hex64", + "name": "", + "offset": 150064, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3967", + "kind": "Hex64", + "name": "", + "offset": 150072, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3968", + "kind": "Hex64", + "name": "", + "offset": 150080, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3969", + "kind": "Hex64", + "name": "", + "offset": 150088, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3970", + "kind": "Hex64", + "name": "", + "offset": 150096, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3971", + "kind": "Hex64", + "name": "", + "offset": 150104, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3972", + "kind": "Hex64", + "name": "", + "offset": 152024, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3973", + "kind": "Hex64", + "name": "", + "offset": 152032, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3974", + "kind": "Hex64", + "name": "", + "offset": 152040, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3975", + "kind": "Hex64", + "name": "", + "offset": 152048, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3976", + "kind": "Hex64", + "name": "", + "offset": 152056, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3977", + "kind": "Hex64", + "name": "", + "offset": 152064, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3978", + "kind": "Hex64", + "name": "", + "offset": 152072, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3979", + "kind": "Hex64", + "name": "", + "offset": 152080, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3980", + "kind": "Hex64", + "name": "serverCmds_0", + "offset": 152088, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3981", + "kind": "Hex64", + "name": "gap_25220", + "offset": 152096, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3982", + "kind": "Hex64", + "name": "", + "offset": 152104, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3983", + "kind": "Hex64", + "name": "", + "offset": 152112, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3984", + "kind": "Hex64", + "name": "", + "offset": 152120, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3985", + "kind": "Hex64", + "name": "", + "offset": 152128, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3986", + "kind": "Hex64", + "name": "", + "offset": 152136, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3987", + "kind": "Hex64", + "name": "", + "offset": 152144, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3988", + "kind": "Hex64", + "name": "", + "offset": 152152, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3989", + "kind": "Hex64", + "name": "", + "offset": 219064, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3990", + "kind": "Hex64", + "name": "", + "offset": 219072, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3991", + "kind": "Hex64", + "name": "", + "offset": 219080, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3992", + "kind": "Hex64", + "name": "", + "offset": 219088, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3993", + "kind": "Hex64", + "name": "", + "offset": 219096, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3994", + "kind": "Hex64", + "name": "", + "offset": 219104, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3995", + "kind": "Hex64", + "name": "", + "offset": 219112, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3996", + "kind": "Hex64", + "name": "", + "offset": 219120, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3997", + "kind": "Int32", + "name": "statsSeq_lo", + "offset": 219128, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3998", + "kind": "Int32", + "name": "statsSeq_hi", + "offset": 219132, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "3999", + "kind": "Hex64", + "name": "statsMem_0", + "offset": 219136, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4000", + "kind": "Hex64", + "name": "statsMem_8", + "offset": 219144, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4001", + "kind": "Hex64", + "name": "statsMem_16", + "offset": 219152, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4002", + "kind": "Hex64", + "name": "statsMem_24", + "offset": 219160, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4003", + "kind": "Hex64", + "name": "gap_35820", + "offset": 219168, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4004", + "kind": "Hex64", + "name": "", + "offset": 219176, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4005", + "kind": "Hex64", + "name": "", + "offset": 219184, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4006", + "kind": "Hex64", + "name": "", + "offset": 219192, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4007", + "kind": "Hex64", + "name": "", + "offset": 219200, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4008", + "kind": "Hex64", + "name": "", + "offset": 219208, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4009", + "kind": "Hex64", + "name": "", + "offset": 219216, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4010", + "kind": "Hex64", + "name": "", + "offset": 219224, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4011", + "kind": "Hex64", + "name": "", + "offset": 219236, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4012", + "kind": "Hex64", + "name": "", + "offset": 219244, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4013", + "kind": "Hex64", + "name": "", + "offset": 219252, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4014", + "kind": "Hex64", + "name": "", + "offset": 219260, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4015", + "kind": "Hex64", + "name": "", + "offset": 219268, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4016", + "kind": "Hex64", + "name": "", + "offset": 219276, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4017", + "kind": "Hex64", + "name": "", + "offset": 219284, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4018", + "kind": "Hex64", + "name": "", + "offset": 219292, + "parentId": "3063", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4938", + "kind": "Int32", + "name": "weaponId", + "offset": 0, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4939", + "kind": "Int32", + "name": "field_04", + "offset": 4, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4940", + "kind": "Int32", + "name": "field_08", + "offset": 8, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4941", + "kind": "Int32", + "name": "field_0C", + "offset": 12, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4942", + "kind": "Int32", + "name": "field_10", + "offset": 16, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4943", + "kind": "Int32", + "name": "variantMask", + "offset": 20, + "parentId": "4937", + "refId": "0", + "strLen": 64 + }, + { + "arrayLen": 1, + "collapsed": false, + "elementKind": "UInt8", + "id": "4944", + "kind": "Int32", + "name": "flags", + "offset": 24, + "parentId": "4937", + "refId": "0", + "strLen": 64 } ], "pointerSize": 4 -} +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 3ed6171..89db02b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -236,6 +236,14 @@ public: class MenuBarStyle : public QProxyStyle { public: using QProxyStyle::QProxyStyle; + void polish(QWidget* w) override { + // Strip OS window border/shadow from QMenu popups — we draw our own + // 1px border in PE_FrameMenu. Same pattern as TypeSelectorPopup. + if (qobject_cast(w)) + w->setWindowFlag(Qt::FramelessWindowHint, true); + QProxyStyle::polish(w); + } + using QProxyStyle::polish; QSize sizeFromContents(ContentsType type, const QStyleOption* opt, const QSize& sz, const QWidget* w) const override { QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w); @@ -247,9 +255,12 @@ public: } int pixelMetric(PixelMetric metric, const QStyleOption* opt, const QWidget* w) const override { - // Kill the 1px frame margin Fusion reserves around QMenu contents + // Reserve 1px for our own menu border (drawn in PE_FrameMenu) if (metric == PM_MenuPanelWidth) - return 0; + return 1; + // Inset menu items from border so hover rect doesn't touch edges + if (metric == PM_MenuHMargin) + return 3; // Thin draggable separator between dock widgets / central widget if (metric == PM_DockWidgetSeparatorExtent) return 1; @@ -257,9 +268,13 @@ public: } void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt, QPainter* p, const QWidget* w) const override { - // Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough - if (elem == PE_FrameMenu) + // Clean 1px border on QMenu (replaces Fusion's 3D bevel + OS shadow) + if (elem == PE_FrameMenu) { + p->setPen(opt->palette.color(QPalette::Dark)); + p->setBrush(Qt::NoBrush); + p->drawRect(opt->rect.adjusted(0, 0, -1, -1)); return; + } // Kill the status bar item frame and panel border if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar) return; @@ -355,7 +370,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) { pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt); pal.setColor(QPalette::ToolTipText, theme.text); pal.setColor(QPalette::Mid, theme.hover); - pal.setColor(QPalette::Dark, theme.background); + pal.setColor(QPalette::Dark, theme.border); pal.setColor(QPalette::Light, theme.textFaint); pal.setColor(QPalette::Link, theme.indHoverSpan); @@ -657,6 +672,15 @@ void MainWindow::createMenus() { tab.ctrl->setCompactColumns(checked); }); + auto* actTreeLines = view->addAction("&Tree Lines"); + actTreeLines->setCheckable(true); + actTreeLines->setChecked(settings.value("treeLines", false).toBool()); + connect(actTreeLines, &QAction::triggered, this, [this](bool checked) { + QSettings("Reclass", "Reclass").setValue("treeLines", checked); + for (auto& tab : m_tabs) + tab.ctrl->setTreeLines(checked); + }); + auto* actRelOfs = view->addAction("R&elative Offsets"); actRelOfs->setCheckable(true); actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool()); @@ -1307,6 +1331,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { // Apply global compact columns setting to new tab ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool()); + ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", false).toBool()); // Give every controller the shared document list for cross-tab type visibility ctrl->setProjectDocuments(&m_allDocs); @@ -2977,6 +3002,30 @@ void MainWindow::createScannerDock() { return ctrl ? ctrl->document()->provider : nullptr; }); + // Wire bounds getter: struct base + size for "Current Struct" filter + m_scannerPanel->setBoundsGetter([this]() -> rcx::ScannerPanel::StructBounds { + auto* ctrl = activeController(); + if (!ctrl) return {}; + auto& tree = ctrl->document()->tree; + uint64_t base = tree.baseAddress; + uint64_t viewRoot = ctrl->viewRootId(); + int span = 0; + if (viewRoot != 0) { + span = tree.structSpan(viewRoot); + } else { + // Compute extent from all top-level nodes + for (int i = 0; i < tree.nodes.size(); i++) { + const auto& n = tree.nodes[i]; + int64_t off = tree.computeOffset(i); + int sz = (n.kind == rcx::NodeKind::Struct || n.kind == rcx::NodeKind::Array) + ? tree.structSpan(n.id) : n.byteSize(); + int64_t end = off + sz; + if (end > span) span = static_cast(end); + } + } + return { base, static_cast(span) }; + }); + // Wire "Go to Address" to rebase the active tab connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) { auto* ctrl = activeController(); diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index f90f0cd..40ec41a 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -1214,9 +1214,13 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) { } if (action == "reset_tracking") { - if (!ctrl) return makeTextResult("No active tab", true); - ctrl->resetChangeTracking(); - return makeTextResult("Value tracking reset. All histories cleared."); + int count = m_mainWindow->tabCount(); + for (int i = 0; i < count; ++i) { + auto* t = m_mainWindow->tabByIndex(i); + if (t && t->ctrl) + t->ctrl->resetChangeTracking(); + } + return makeTextResult(QStringLiteral("Value tracking reset on all %1 tabs.").arg(count)); } return makeTextResult("Unknown action: " + action, true); diff --git a/src/rcxtooltip.h b/src/rcxtooltip.h new file mode 100644 index 0000000..9ee8a95 --- /dev/null +++ b/src/rcxtooltip.h @@ -0,0 +1,241 @@ +#pragma once +#include "themes/thememanager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TIP_LOG(...) do { \ + FILE* _f = fopen("E:/game_dev/util/reclass2027-main/build/tip_trace.log", "a"); \ + if (_f) { fprintf(_f, __VA_ARGS__); fclose(_f); } \ +} while(0) + +namespace rcx { + +class RcxTooltip : public QWidget { +public: + static RcxTooltip* instance() { + static RcxTooltip* s = nullptr; + if (!s) { + s = new RcxTooltip; + QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + s, [](const rcx::Theme&) { /* colors read live in paintEvent */ }); + } + return s; + } + + void showFor(QWidget* trigger, const QString& text) { + if (!trigger || text.isEmpty()) { + TIP_LOG("[TIP] showFor: null trigger or empty text -- dismiss\n"); + dismiss(); return; + } + + // Same widget+text already showing — do nothing (prevents teleport) + if (m_trigger == trigger && m_text == text && isVisible()) { + TIP_LOG("[TIP] showFor: same widget+text, already visible -- skip\n"); + return; + } + + TIP_LOG("[TIP] showFor: text='%s' trigger=%p class=%s\n", + qPrintable(text), (void*)trigger, trigger->metaObject()->className()); + + // Cancel pending dismiss + if (m_dismissTimer) m_dismissTimer->stop(); + + m_trigger = trigger; + m_text = text; + + m_label->setText(text); + m_label->adjustSize(); + + // ── Size: label + padding + arrow ── + const int pad = 8; + const int vpad = 4; + int bodyW = m_label->sizeHint().width() + pad * 2; + int bodyH = m_label->sizeHint().height() + vpad * 2; + int totalW = bodyW; + int totalH = bodyH + kArrowH; + + // ── Position relative to trigger widget ── + QRect trigGlobal = QRect(trigger->mapToGlobal(QPoint(0, 0)), trigger->size()); + int trigCenterX = trigGlobal.center().x(); + + QScreen* screen = QApplication::screenAt(trigGlobal.center()); + QRect scr = screen ? screen->availableGeometry() : QRect(0, 0, 1920, 1080); + + // Default: above the trigger + m_arrowDown = true; + int x = trigCenterX - totalW / 2; + int y = trigGlobal.top() - totalH - kGap; + + // Flip below if not enough room above + if (y < scr.top()) { + m_arrowDown = false; + y = trigGlobal.bottom() + kGap; + } + + // Clamp horizontally + if (x < scr.left()) x = scr.left() + 2; + if (x + totalW > scr.right()) x = scr.right() - totalW - 2; + + // Arrow X in local coords + m_arrowLocalX = trigCenterX - x; + m_arrowLocalX = qBound(kArrowHalfW + 4, m_arrowLocalX, totalW - kArrowHalfW - 4); + + // Position label inside the body + if (m_arrowDown) + m_label->move(pad, vpad); + else + m_label->move(pad, kArrowH + vpad); + + m_bodyRect = m_arrowDown + ? QRect(0, 0, bodyW, bodyH) + : QRect(0, kArrowH, bodyW, bodyH); + + setFixedSize(totalW, totalH); + move(x, y); + + if (!isVisible()) { + TIP_LOG("[TIP] showFor: showing at (%d,%d) size=%dx%d arrowDown=%d arrowX=%d\n", + x, y, totalW, totalH, m_arrowDown, m_arrowLocalX); + setWindowOpacity(0.0); + show(); + raise(); + // Fade in + auto* anim = new QPropertyAnimation(this, "windowOpacity", this); + anim->setDuration(80); + anim->setStartValue(0.0); + anim->setEndValue(1.0); + anim->setEasingCurve(QEasingCurve::OutCubic); + anim->start(QAbstractAnimation::DeleteWhenStopped); + } else { + TIP_LOG("[TIP] showFor: already visible, updating\n"); + update(); + } + } + + void dismiss() { + TIP_LOG("[TIP] dismiss: wasVisible=%d\n", isVisible()); + if (m_dismissTimer) m_dismissTimer->stop(); + if (isVisible()) hide(); + m_trigger = nullptr; + } + + // Schedule dismiss with a delay — but only if the cursor has truly + // left the trigger+tooltip zone. Qt fires synthetic Leave events + // when a tooltip window appears above the trigger; we must ignore those. + void scheduleDismiss() { + if (m_trigger) { + QPoint cursor = QCursor::pos(); + QRect trigRect(m_trigger->mapToGlobal(QPoint(0, 0)), m_trigger->size()); + QRect tipRect(pos(), size()); + QRect zone = trigRect.united(tipRect).adjusted(-4, -4, 4, 4); + bool inside = zone.contains(cursor); + TIP_LOG("[TIP] scheduleDismiss: cursor=(%d,%d) zone=(%d,%d %dx%d) inside=%d\n", + cursor.x(), cursor.y(), + zone.x(), zone.y(), zone.width(), zone.height(), inside); + if (inside) + return; // cursor still inside — ignore spurious Leave + } + if (!m_dismissTimer) { + m_dismissTimer = new QTimer(this); + m_dismissTimer->setSingleShot(true); + connect(m_dismissTimer, &QTimer::timeout, this, &RcxTooltip::dismiss); + } + m_dismissTimer->start(100); + } + + QWidget* currentTrigger() const { return m_trigger; } + + // ── Geometry accessors (for testing) ── + bool arrowPointsDown() const { return m_arrowDown; } + int arrowLocalX() const { return m_arrowLocalX; } + QRect bodyRect() const { return m_bodyRect; } + QString currentText() const { return m_text; } + + // Constants exposed for testing + static constexpr int kArrowH = 6; + static constexpr int kArrowHalfW = 6; + static constexpr int kGap = 2; + +protected: + void paintEvent(QPaintEvent*) override { + TIP_LOG("[TIP] paintEvent: size=%dx%d bodyRect=(%d,%d %dx%d)\n", + width(), height(), + m_bodyRect.x(), m_bodyRect.y(), m_bodyRect.width(), m_bodyRect.height()); + const auto& theme = ThemeManager::instance().current(); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Fill entire widget with the tooltip background first + // (no WA_TranslucentBackground, so unpainted areas would be opaque garbage) + p.fillRect(rect(), theme.backgroundAlt); + + // Build path: rounded body + triangle arrow + QPainterPath path; + path.addRoundedRect(QRectF(m_bodyRect), 4.0, 4.0); + + // Triangle arrow + QPolygonF arrow; + if (m_arrowDown) { + int ay = m_bodyRect.bottom(); + arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay) + << QPointF(m_arrowLocalX, ay + kArrowH) + << QPointF(m_arrowLocalX + kArrowHalfW, ay); + } else { + int ay = kArrowH; + arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay) + << QPointF(m_arrowLocalX, 0) + << QPointF(m_arrowLocalX + kArrowHalfW, ay); + } + QPainterPath arrowPath; + arrowPath.addPolygon(arrow); + arrowPath.closeSubpath(); + path = path.united(arrowPath); + + // Stroke the shape border + p.setPen(QPen(theme.border, 1.0)); + p.setBrush(theme.backgroundAlt); + p.drawPath(path); + } + +private: + explicit RcxTooltip() + : QWidget(nullptr, Qt::ToolTip | Qt::FramelessWindowHint) + { + // NOTE: WA_TranslucentBackground removed — it breaks under DWM dark mode + // (DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE kills layered compositing) + setAttribute(Qt::WA_ShowWithoutActivating); + setAutoFillBackground(false); // we paint everything ourselves in paintEvent + + m_label = new QLabel(this); + m_label->setAlignment(Qt::AlignCenter); + updateLabelStyle(); + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, [this](const rcx::Theme&) { updateLabelStyle(); }); + } + + void updateLabelStyle() { + const auto& theme = ThemeManager::instance().current(); + m_label->setStyleSheet( + QStringLiteral("QLabel { color: %1; background: transparent; padding: 0; }") + .arg(theme.text.name())); + } + + QLabel* m_label = nullptr; + QWidget* m_trigger = nullptr; + QString m_text; + QTimer* m_dismissTimer = nullptr; + bool m_arrowDown = true; + int m_arrowLocalX = 0; + QRect m_bodyRect; +}; + +} // namespace rcx diff --git a/src/scanner.cpp b/src/scanner.cpp index 0261963..7ceb25c 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -489,13 +489,21 @@ QVector ScanEngine::runScan(std::shared_ptr prov, const char* msk = isUnknown ? nullptr : req.mask.constData(); const int alignment = qMax(1, req.alignment); const int valSize = isUnknown ? req.valueSize : patternLen; + const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) && + req.endAddress > req.startAddress; // Pre-compute total bytes for progress uint64_t totalBytes = 0; for (const auto& r : regions) { if (req.filterExecutable && !r.executable) continue; if (req.filterWritable && !r.writable) continue; - totalBytes += r.size; + uint64_t rStart = r.base, rEnd = r.base + r.size; + if (hasRange) { + if (rEnd <= req.startAddress || rStart >= req.endAddress) continue; + rStart = qMax(rStart, req.startAddress); + rEnd = qMin(rEnd, req.endAddress); + } + totalBytes += rEnd - rStart; } qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions"; @@ -513,21 +521,35 @@ QVector ScanEngine::runScan(std::shared_ptr prov, if (req.filterExecutable && !region.executable) continue; if (req.filterWritable && !region.writable) continue; - if ((uint64_t)patternLen > region.size) { - scannedBytes += region.size; + // Clip region to requested address range + uint64_t regStart = region.base; + uint64_t regEnd = region.base + region.size; + if (hasRange) { + if (regEnd <= req.startAddress || regStart >= req.endAddress) { + // Entirely outside range — skip + continue; + } + regStart = qMax(regStart, req.startAddress); + regEnd = qMin(regEnd, req.endAddress); + } + uint64_t regSize = regEnd - regStart; + + if ((uint64_t)patternLen > regSize) { + scannedBytes += regSize; continue; } const int overlap = patternLen - 1; - QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized); + QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized); + uint64_t regOffset = regStart - region.base; // offset within provider region - for (uint64_t off = 0; off < region.size; ) { + for (uint64_t off = 0; off < regSize; ) { if (m_abort.load()) break; - uint64_t remaining = region.size - off; + uint64_t remaining = regSize - off; int readLen = (int)qMin((uint64_t)chunk.size(), remaining); - if (!prov->read(region.base + off, chunk.data(), readLen)) { + if (!prov->read(regStart + off, chunk.data(), readLen)) { // Skip unreadable chunk off += readLen; scannedBytes += readLen; @@ -541,7 +563,7 @@ QVector ScanEngine::runScan(std::shared_ptr prov, // Unknown value: capture every aligned address for (int i = 0; i <= scanEnd; i += alignment) { ScanResult r; - r.address = region.base + off + (uint64_t)i; + r.address = regStart + off + (uint64_t)i; r.scanValue = QByteArray(data + i, valSize); results.append(r); @@ -560,7 +582,7 @@ QVector ScanEngine::runScan(std::shared_ptr prov, } if (match) { ScanResult r; - r.address = region.base + off + (uint64_t)i; + r.address = regStart + off + (uint64_t)i; r.regionModule = region.moduleName; r.scanValue = QByteArray(data + i, qMin(16, readLen - i)); results.append(r); diff --git a/src/scanner.h b/src/scanner.h index 9f988a3..491a8a3 100644 --- a/src/scanner.h +++ b/src/scanner.h @@ -46,6 +46,9 @@ struct ScanRequest { ScanCondition condition = ScanCondition::ExactValue; int valueSize = 4; // bytes per value (for unknown scans) + + uint64_t startAddress = 0; // 0 = no limit (scan all regions) + uint64_t endAddress = 0; // 0 = no limit (scan all regions) }; struct ScanResult { diff --git a/src/scannerpanel.cpp b/src/scannerpanel.cpp index 562d7c3..1665ba4 100644 --- a/src/scannerpanel.cpp +++ b/src/scannerpanel.cpp @@ -124,6 +124,9 @@ ScannerPanel::ScannerPanel(QWidget* parent) m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this); filterRow->addWidget(m_writeCheck); + m_structOnlyCheck = new QCheckBox(QStringLiteral("Current Struct"), this); + filterRow->addWidget(m_structOnlyCheck); + filterRow->addStretch(); m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")), @@ -257,6 +260,10 @@ void ScannerPanel::setProviderGetter(ProviderGetter getter) { m_providerGetter = std::move(getter); } +void ScannerPanel::setBoundsGetter(BoundsGetter getter) { + m_boundsGetter = std::move(getter); +} + void ScannerPanel::setEditorFont(const QFont& font) { m_resultTable->setFont(font); QFontMetrics fm(font); @@ -278,6 +285,7 @@ void ScannerPanel::setEditorFont(const QFont& font) { m_valueLabel->setFont(font); m_execCheck->setFont(font); m_writeCheck->setFont(font); + m_structOnlyCheck->setFont(font); m_updateBtn->setFont(font); updateComboWidth(); } @@ -398,6 +406,14 @@ ScanRequest ScannerPanel::buildRequest() { req.filterExecutable = m_execCheck->isChecked(); req.filterWritable = m_writeCheck->isChecked(); + if (m_structOnlyCheck->isChecked() && m_boundsGetter) { + auto bounds = m_boundsGetter(); + if (bounds.size > 0) { + req.startAddress = bounds.start; + req.endAddress = bounds.start + bounds.size; + } + } + return req; } @@ -750,6 +766,7 @@ void ScannerPanel::applyTheme(const Theme& theme) { cp.setColor(QPalette::WindowText, theme.textDim); m_execCheck->setPalette(cp); m_writeCheck->setPalette(cp); + m_structOnlyCheck->setPalette(cp); // Buttons QString btnStyle = QStringLiteral( diff --git a/src/scannerpanel.h b/src/scannerpanel.h index 05dff50..c7e40b5 100644 --- a/src/scannerpanel.h +++ b/src/scannerpanel.h @@ -34,6 +34,10 @@ public: using ProviderGetter = std::function()>; void setProviderGetter(ProviderGetter getter); + struct StructBounds { uint64_t start = 0; uint64_t size = 0; }; + using BoundsGetter = std::function; + void setBoundsGetter(BoundsGetter getter); + void setEditorFont(const QFont& font); void applyTheme(const Theme& theme); @@ -54,6 +58,7 @@ public: ScanEngine* engine() const { return m_engine; } QComboBox* condCombo() const { return m_condCombo; } QLabel* condLabel() const { return m_condLabel; } + QCheckBox* structOnlyCheck() const { return m_structOnlyCheck; } signals: void goToAddress(uint64_t address); @@ -90,6 +95,7 @@ private: // Filters QCheckBox* m_execCheck; QCheckBox* m_writeCheck; + QCheckBox* m_structOnlyCheck; // Actions QPushButton* m_scanBtn; @@ -106,6 +112,7 @@ private: // Engine ScanEngine* m_engine; ProviderGetter m_providerGetter; + BoundsGetter m_boundsGetter; QVector m_results; int m_lastScanMode = 0; // 0=signature, 1=value ValueType m_lastValueType = ValueType::Int32; diff --git a/src/themes/defaults/mid.json b/src/themes/defaults/mid.json index 9b90ede..56c31d2 100644 --- a/src/themes/defaults/mid.json +++ b/src/themes/defaults/mid.json @@ -10,8 +10,8 @@ "textDim": "#505C74", "textMuted": "#384258", "textFaint": "#2C3448", - "hover": "#121720", - "selected": "#121720", + "hover": "#181E2A", + "selected": "#1A2D4A", "selection": "#1A2038", "syntaxKeyword": "#5688C0", "syntaxNumber": "#90B480", diff --git a/src/themes/defaults/tw.json b/src/themes/defaults/tw.json new file mode 100644 index 0000000..a5a211c --- /dev/null +++ b/src/themes/defaults/tw.json @@ -0,0 +1,32 @@ +{ + "name": "Light", + "background": "#e8e8ec", + "backgroundAlt": "#dcdce0", + "surface": "#d4d4d8", + "border": "#b8b8be", + "borderFocused": "#6870a0", + "button": "#ccccd0", + "text": "#1b1b22", + "textDim": "#5c5c68", + "textMuted": "#84848e", + "textFaint": "#a8a8b0", + "hover": "#d8d8de", + "selected": "#d0d0d8", + "selection": "#b4c8e8", + "syntaxKeyword": "#4455aa", + "syntaxNumber": "#2a7a4c", + "syntaxString": "#9a4040", + "syntaxComment": "#6a7a6a", + "syntaxPreproc": "#787880", + "syntaxType": "#2e7a8a", + "indHoverSpan": "#5a68a0", + "indCmdPill": "#dcdce0", + "indDataChanged": "#2a7a4c", + "indHeatCold": "#6a6a30", + "indHeatWarm": "#a06828", + "indHeatHot": "#b83030", + "indHintGreen": "#387a44", + "markerPtr": "#b83030", + "markerCycle": "#9a7010", + "markerError": "#e8c8c8" +} diff --git a/src/themes/defaults/warm.json b/src/themes/defaults/warm.json index 6e54877..7854d63 100644 --- a/src/themes/defaults/warm.json +++ b/src/themes/defaults/warm.json @@ -15,8 +15,8 @@ "selection": "#21213A", "syntaxKeyword": "#AA9565", "syntaxNumber": "#AAA98C", - "syntaxString": "#6B3B21", - "syntaxComment": "#464646", + "syntaxString": "#C0825A", + "syntaxComment": "#8A8878", "syntaxPreproc": "#AA9565", "syntaxType": "#6B959F", "indHoverSpan": "#AA9565", @@ -25,8 +25,8 @@ "indHeatCold": "#C4A44A", "indHeatWarm": "#AA9565", "indHeatHot": "#A05040", - "indHintGreen": "#464646", - "markerPtr": "#6B3B21", + "indHintGreen": "#688A58", + "markerPtr": "#B85A42", "markerCycle": "#AA9565", "markerError": "#3C2121" } diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index d42b7ce..bfa8abb 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -2627,6 +2627,122 @@ private slots: QVERIFY2(result.text.contains(QStringLiteral("\u2192")), qPrintable("Expected arrow (\u2192) in text:\n" + result.text)); } + void testTreeLinesDepth2() { + // Diagnostic test: verify tree chars at depth 2+ with hex64 nodes + // (matches user's actual scenario — Hex64 inside pointer expansion) + NodeTree tree; + tree.baseAddress = 0; + + // Root struct "Unnamed" + Node root; + root.kind = NodeKind::Struct; + root.name = "Unnamed"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // First child: hex64 at depth 1 + Node f1; + f1.kind = NodeKind::Hex64; + f1.name = ""; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + // Ref struct "NewClass" (separate root-level definition) + Node inner; + inner.kind = NodeKind::Struct; + inner.name = "NewClass"; + inner.parentId = 0; + inner.offset = 200; + int ii = tree.addNode(inner); + uint64_t innerId = tree.nodes[ii].id; + + // hex64 children of NewClass + Node if1; + if1.kind = NodeKind::Hex64; + if1.name = ""; + if1.parentId = innerId; + if1.offset = 0; + tree.addNode(if1); + + Node if2; + if2.kind = NodeKind::Hex64; + if2.name = ""; + if2.parentId = innerId; + if2.offset = 8; + tree.addNode(if2); + + Node if3; + if3.kind = NodeKind::Hex64; + if3.name = ""; + if3.parentId = innerId; + if3.offset = 16; + tree.addNode(if3); + + // Pointer in root referencing NewClass + Node ptr; + ptr.kind = NodeKind::Pointer64; + ptr.name = "field_0008"; + ptr.parentId = rootId; + ptr.offset = 8; + ptr.refId = innerId; + tree.addNode(ptr); + + // Last child: hex64 at depth 1 + Node f2; + f2.kind = NodeKind::Hex64; + f2.name = ""; + f2.parentId = rootId; + f2.offset = 16; + tree.addNode(f2); + + // Provider with pointer value + QByteArray data(256, '\0'); + uint64_t ptrVal = 100; + memcpy(data.data() + 8, &ptrVal, 8); + BufferProvider prov(data); + + // Compose WITH tree lines + ComposeResult result = compose(tree, prov, 0, false, true); + + QStringList lines = result.text.split('\n'); + + // Print output with char codes for debugging + qDebug() << "=== Tree lines compose output (hex64 scenario) ==="; + for (int i = 0; i < lines.size(); i++) { + // Also show hex of first 15 chars to see tree chars + QString hexChars; + for (int c = 0; c < qMin(15, lines[i].size()); c++) + hexChars += QString("U+%1 ").arg(lines[i][c].unicode(), 4, 16, QChar('0')); + qDebug().noquote() << QString("[%1] d=%2 k=%3: %4") + .arg(i, 2).arg(result.meta[i].depth).arg((int)result.meta[i].lineKind).arg(lines[i]); + qDebug().noquote() << QString(" hex: %1").arg(hexChars); + } + qDebug() << "=== end ==="; + + // Verify depth-2 lines contain tree chars + QChar vertLine(0x2502); // │ + QChar tee(0x251C); // ├ + QChar corner(0x2514); // └ + + bool foundDepth2TreeChar = false; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].depth == 2 + && result.meta[i].lineKind != LineKind::Footer) { + bool has = lines[i].contains(vertLine) + || lines[i].contains(tee) + || lines[i].contains(corner); + if (has) foundDepth2TreeChar = true; + QVERIFY2(has, + qPrintable(QString("Depth-2 line %1 missing tree chars: %2") + .arg(i).arg(lines[i]))); + } + } + QVERIFY2(foundDepth2TreeChar, + qPrintable("No depth-2 lines with tree chars found:\n" + result.text)); + } }; QTEST_MAIN(TestCompose) diff --git a/tests/test_dbgdump.cpp b/tests/test_dbgdump.cpp new file mode 100644 index 0000000..07bc5bc --- /dev/null +++ b/tests/test_dbgdump.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) +{ + const char* dumpPath = "F:\\MEMORY_EaService2024.DMP"; + if (argc > 1) dumpPath = argv[1]; + + HRESULT hrCom = CoInitializeEx(NULL, COINIT_MULTITHREADED); + printf("CoInitializeEx: 0x%08lX\n", hrCom); + fflush(stdout); + + IDebugClient* client = nullptr; + HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client); + printf("DebugCreate: 0x%08lX, client=%p\n", hr, (void*)client); + fflush(stdout); + + if (FAILED(hr) || !client) { + printf("FAILED to create debug client\n"); + if (SUCCEEDED(hrCom)) CoUninitialize(); + return 1; + } + + printf("Opening dump: %s\n", dumpPath); + fflush(stdout); + hr = client->OpenDumpFile(dumpPath); + printf("OpenDumpFile: 0x%08lX\n", hr); + fflush(stdout); + + if (FAILED(hr)) { + printf("FAILED to open dump\n"); + client->Release(); + if (SUCCEEDED(hrCom)) CoUninitialize(); + return 1; + } + + IDebugControl* ctrl = nullptr; + client->QueryInterface(IID_IDebugControl, (void**)&ctrl); + + if (ctrl) { + printf("WaitForEvent(10s)...\n"); + fflush(stdout); + hr = ctrl->WaitForEvent(0, 10000); + printf("WaitForEvent: 0x%08lX\n", hr); + fflush(stdout); + + ULONG debugClass = 0, debugQual = 0; + hr = ctrl->GetDebuggeeType(&debugClass, &debugQual); + printf("GetDebuggeeType: 0x%08lX, class=%lu, qualifier=%lu\n", + hr, debugClass, debugQual); + printf(" -> %s\n", debugQual >= 1024 ? "DUMP" : "LIVE"); + fflush(stdout); + } + + IDebugDataSpaces* ds = nullptr; + client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds); + + IDebugSymbols* sym = nullptr; + client->QueryInterface(IID_IDebugSymbols, (void**)&sym); + + if (sym) { + ULONG numMods = 0, numUnloaded = 0; + hr = sym->GetNumberModules(&numMods, &numUnloaded); + printf("GetNumberModules: 0x%08lX, loaded=%lu, unloaded=%lu\n", + hr, numMods, numUnloaded); + fflush(stdout); + + if (numMods > 0) { + ULONG64 base = 0; + hr = sym->GetModuleByIndex(0, &base); + printf("Module[0] base: 0x%llX (hr=0x%08lX)\n", base, hr); + fflush(stdout); + + if (SUCCEEDED(hr) && base && ds) { + uint8_t buf[16] = {}; + ULONG got = 0; + hr = ds->ReadVirtual(base, buf, 16, &got); + printf("ReadVirtual(0x%llX, 16): hr=0x%08lX, got=%lu\n", base, hr, got); + printf(" data: "); + for (int i = 0; i < 16; i++) printf("%02X ", buf[i]); + printf("\n"); + fflush(stdout); + } + } + } + + // Try reading kernel base directly + uint64_t ntBase = 0xfffff80123c00000ULL; + if (ds) { + uint8_t buf[16] = {}; + ULONG got = 0; + hr = ds->ReadVirtual(ntBase, buf, 16, &got); + printf("ReadVirtual(nt base 0x%llX, 16): hr=0x%08lX, got=%lu\n", ntBase, hr, got); + printf(" data: "); + for (int i = 0; i < 16; i++) printf("%02X ", buf[i]); + printf("\n"); + fflush(stdout); + } + + if (sym) sym->Release(); + if (ds) ds->Release(); + if (ctrl) ctrl->Release(); + client->DetachProcesses(); + client->Release(); + + printf("Done.\n"); + if (SUCCEEDED(hrCom)) CoUninitialize(); + return 0; +} diff --git a/tests/test_scanner.cpp b/tests/test_scanner.cpp index d5fd78d..09da93e 100644 --- a/tests/test_scanner.cpp +++ b/tests/test_scanner.cpp @@ -1072,6 +1072,120 @@ private slots: auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 7); // 8 - 2 + 1 = 7 positions } + + // ═══════════════════════════════════════════════════════════════════ + // Address range filtering — "Current Struct" support + // ═══════════════════════════════════════════════════════════════════ + + void scan_addressRangeNoLimit() { + // startAddress=0, endAddress=0 → scan all (default behavior unchanged) + QByteArray data(32, '\x00'); + data[8] = '\xAA'; data[16] = '\xAA'; data[24] = '\xAA'; + auto prov = std::make_shared(data); + ScanEngine engine; + QSignalSpy finSpy(&engine, &ScanEngine::finished); + + ScanRequest req; + req.pattern = QByteArray("\xAA", 1); + req.mask = QByteArray("\xFF", 1); + + engine.start(prov, req); + QVERIFY(finSpy.wait(5000)); + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 3); // all 3 found + } + + void scan_addressRangeClipsResults() { + // Only scan addresses [8, 20) — should find match at offset 8 and 16 but not 24 + QByteArray data(32, '\x00'); + data[8] = '\xAA'; data[16] = '\xAA'; data[24] = '\xAA'; + auto prov = std::make_shared(data); + ScanEngine engine; + QSignalSpy finSpy(&engine, &ScanEngine::finished); + + ScanRequest req; + req.pattern = QByteArray("\xAA", 1); + req.mask = QByteArray("\xFF", 1); + req.startAddress = 8; + req.endAddress = 20; + + engine.start(prov, req); + QVERIFY(finSpy.wait(5000)); + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 2); + QCOMPARE(results[0].address, (uint64_t)8); + QCOMPARE(results[1].address, (uint64_t)16); + } + + void scan_addressRangeOutsideData() { + // Range entirely outside data → no results + QByteArray data(16, '\xAA'); + auto prov = std::make_shared(data); + ScanEngine engine; + QSignalSpy finSpy(&engine, &ScanEngine::finished); + + ScanRequest req; + req.pattern = QByteArray("\xAA", 1); + req.mask = QByteArray("\xFF", 1); + req.startAddress = 100; + req.endAddress = 200; + + engine.start(prov, req); + QVERIFY(finSpy.wait(5000)); + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 0); + } + + void scan_addressRangeWithRegions() { + // Two regions: [1000, 1016) and [2000, 2016). Range [1000, 1020) clips to first region only. + QByteArray data(4096, '\x00'); + // Place \xBB at offset 1000 and 2000 + data[1000] = '\xBB'; + data[2000] = '\xBB'; + + QVector regions; + { MemoryRegion r; r.base = 1000; r.size = 16; r.readable = true; r.writable = true; regions.append(r); } + { MemoryRegion r; r.base = 2000; r.size = 16; r.readable = true; r.writable = true; regions.append(r); } + + auto prov = std::make_shared(data, regions); + ScanEngine engine; + QSignalSpy finSpy(&engine, &ScanEngine::finished); + + ScanRequest req; + req.pattern = QByteArray("\xBB", 1); + req.mask = QByteArray("\xFF", 1); + req.startAddress = 1000; + req.endAddress = 1020; + + engine.start(prov, req); + QVERIFY(finSpy.wait(5000)); + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 1); + QCOMPARE(results[0].address, (uint64_t)1000); + } + + void scan_unknownWithAddressRange() { + // Unknown scan with address range should only capture within range + QByteArray data(32, '\x42'); + auto prov = std::make_shared(data); + ScanEngine engine; + QSignalSpy finSpy(&engine, &ScanEngine::finished); + + ScanRequest req; + req.condition = ScanCondition::UnknownValue; + req.valueSize = 4; + req.alignment = 4; + req.startAddress = 8; + req.endAddress = 24; + + engine.start(prov, req); + QVERIFY(finSpy.wait(5000)); + auto results = finSpy.first().first().value>(); + // Range [8, 24) = 16 bytes, alignment 4, valueSize 4 → offsets 8, 12, 16, 20 = 4 results + QCOMPARE(results.size(), 4); + QCOMPARE(results[0].address, (uint64_t)8); + QCOMPARE(results[3].address, (uint64_t)20); + } }; QTEST_MAIN(TestScanner) diff --git a/tests/test_scanner_ui.cpp b/tests/test_scanner_ui.cpp index 7676e9b..638bec3 100644 --- a/tests/test_scanner_ui.cpp +++ b/tests/test_scanner_ui.cpp @@ -1103,6 +1103,89 @@ private slots: // Provider getter is lazy (captures at scan time) // ═══════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════ + // "Current Struct" checkbox + // ═══════════════════════════════════════════════════════════════════ + + void structOnly_checkboxExists() { + QVERIFY(m_panel->structOnlyCheck() != nullptr); + QCOMPARE(m_panel->structOnlyCheck()->isChecked(), false); + QCOMPARE(m_panel->structOnlyCheck()->text(), QStringLiteral("Current Struct")); + } + + void structOnly_setsAddressRange() { + // Set up a bounds getter that returns a known range + m_panel->setBoundsGetter([]() -> ScannerPanel::StructBounds { + return { 0x1000, 0x200 }; + }); + + // Set up a simple buffer provider + QByteArray data(0x2000, '\x00'); + data[0x1000] = '\xCC'; + data[0x1100] = '\xCC'; + data[0x1500] = '\xCC'; // outside bounds (0x1000 + 0x200 = 0x1200) + auto prov = std::make_shared(data); + m_panel->setProviderGetter([prov]() { return prov; }); + + // Enable struct-only mode + m_panel->structOnlyCheck()->setChecked(true); + + // Scan for \xCC + m_panel->patternEdit()->setText("CC"); + QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); + QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); + QVERIFY(finSpy.wait(5000)); + QApplication::processEvents(); + + // Should only find results within [0x1000, 0x1200) + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 2); + } + + void structOnly_uncheckedScansAll() { + // Same setup but with checkbox unchecked — should find all 3 + m_panel->setBoundsGetter([]() -> ScannerPanel::StructBounds { + return { 0x1000, 0x200 }; + }); + + QByteArray data(0x2000, '\x00'); + data[0x1000] = '\xCC'; + data[0x1100] = '\xCC'; + data[0x1500] = '\xCC'; + auto prov = std::make_shared(data); + m_panel->setProviderGetter([prov]() { return prov; }); + + m_panel->structOnlyCheck()->setChecked(false); // unchecked + + m_panel->patternEdit()->setText("CC"); + QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); + QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); + QVERIFY(finSpy.wait(5000)); + QApplication::processEvents(); + + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 3); + } + + void structOnly_noBoundsGetterIgnored() { + // No bounds getter set — checkbox checked but no effect + QByteArray data(16, '\xDD'); + auto prov = std::make_shared(data); + m_panel->setProviderGetter([prov]() { return prov; }); + + m_panel->structOnlyCheck()->setChecked(true); + // Don't set bounds getter + + m_panel->patternEdit()->setText("DD"); + QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); + QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); + QVERIFY(finSpy.wait(5000)); + QApplication::processEvents(); + + auto results = finSpy.first().first().value>(); + QCOMPARE(results.size(), 16); // all 16 bytes match + } + void providerGetter_lazy() { auto prov1 = std::make_shared(QByteArray(16, '\xAA')); auto prov2 = std::make_shared(QByteArray(16, '\xBB')); diff --git a/tests/test_tooltip.cpp b/tests/test_tooltip.cpp new file mode 100644 index 0000000..988e143 --- /dev/null +++ b/tests/test_tooltip.cpp @@ -0,0 +1,432 @@ +#include +#include +#include +#include +#include +#include "rcxtooltip.h" +#include "themes/thememanager.h" + +using namespace rcx; + +// ───────────────────────────────────────────────────────────────── +// Test suite for the RcxTooltip callout widget +// +// These tests verify both geometry math AND real-world behavior: +// - Actual pixel rendering (catches WA_TranslucentBackground failures) +// - Leave-event resilience (catches spurious dismiss on tooltip popup) +// - Dismiss correctness (cursor truly leaves trigger zone) +// ───────────────────────────────────────────────────────────────── +class TestTooltip : public QObject { + Q_OBJECT + +private: + QWidget* m_window = nullptr; + QPushButton* m_btnTop = nullptr; + QPushButton* m_btnMid = nullptr; + QPushButton* m_btnLeft = nullptr; + QPushButton* m_btnRight= nullptr; + + void showAndProcess(QWidget* trigger, const QString& text) { + RcxTooltip::instance()->showFor(trigger, text); + // Process events + allow paint to complete + QCoreApplication::processEvents(); + QTest::qWait(20); + QCoreApplication::processEvents(); + } + + // Count non-transparent pixels in a QImage region + int countOpaquePixels(const QImage& img, const QRect& region) { + int count = 0; + QRect r = region.intersected(img.rect()); + for (int y = r.top(); y <= r.bottom(); ++y) + for (int x = r.left(); x <= r.right(); ++x) + if (qAlpha(img.pixel(x, y)) > 0) + ++count; + return count; + } + +private slots: + void initTestCase() { + m_window = new QWidget; + m_window->setFixedSize(800, 600); + + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + m_window->move(avail.center() - QPoint(400, 300)); + + m_btnMid = new QPushButton("Middle", m_window); + m_btnMid->setFixedSize(80, 24); + m_btnMid->move(360, 288); + + m_btnTop = new QPushButton("Top", m_window); + m_btnTop->setFixedSize(80, 24); + m_btnTop->move(360, 0); + + m_btnLeft = new QPushButton("Left", m_window); + m_btnLeft->setFixedSize(80, 24); + m_btnLeft->move(0, 288); + + m_btnRight = new QPushButton("Right", m_window); + m_btnRight->setFixedSize(80, 24); + m_btnRight->move(720, 288); + + m_window->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_window)); + } + + void cleanupTestCase() { + RcxTooltip::instance()->dismiss(); + delete m_window; + m_window = nullptr; + } + + void cleanup() { + RcxTooltip::instance()->dismiss(); + QCoreApplication::processEvents(); + } + + // ── Singleton ── + void testSingleton() { + QCOMPARE(RcxTooltip::instance(), RcxTooltip::instance()); + } + + // ── Basic show/dismiss ── + void testShowAndDismiss() { + auto* tip = RcxTooltip::instance(); + QVERIFY(!tip->isVisible()); + + showAndProcess(m_btnMid, "Hello"); + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Hello")); + QCOMPARE(tip->currentTrigger(), m_btnMid); + + tip->dismiss(); + QVERIFY(!tip->isVisible()); + QVERIFY(tip->currentTrigger() == nullptr); + } + + // ── Empty text / null trigger = dismiss ── + void testEmptyTextDismisses() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Test"); + QVERIFY(tip->isVisible()); + showAndProcess(m_btnMid, ""); + QVERIFY(!tip->isVisible()); + } + + void testNullTriggerDismisses() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Test"); + QVERIFY(tip->isVisible()); + showAndProcess(nullptr, "Test"); + QVERIFY(!tip->isVisible()); + } + + // ── Arrow direction ── + void testArrowDownByDefault() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Default placement"); + QVERIFY(tip->isVisible()); + QVERIFY(tip->arrowPointsDown()); + + QRect trigGlobal(m_btnMid->mapToGlobal(QPoint(0,0)), m_btnMid->size()); + int tipBottom = tip->y() + tip->height(); + QVERIFY2(tipBottom <= trigGlobal.top() + RcxTooltip::kGap + 2, + qPrintable(QStringLiteral("tipBottom=%1 trigTop=%2") + .arg(tipBottom).arg(trigGlobal.top()))); + } + + void testArrowFlipsAtScreenTop() { + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + QPoint oldPos = m_window->pos(); + m_window->move(avail.center().x() - 400, avail.top()); + QCoreApplication::processEvents(); + + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnTop, "Flipped"); + QVERIFY(tip->isVisible()); + QVERIFY2(!tip->arrowPointsDown(), + "Expected arrow to flip upward when trigger is near screen top"); + + QRect trigGlobal(m_btnTop->mapToGlobal(QPoint(0,0)), m_btnTop->size()); + QVERIFY2(tip->y() >= trigGlobal.bottom(), + qPrintable(QStringLiteral("tipY=%1 trigBottom=%2") + .arg(tip->y()).arg(trigGlobal.bottom()))); + + m_window->move(oldPos); + QCoreApplication::processEvents(); + } + + // ── Arrow centering ── + void testArrowCenteredOnTrigger() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Center"); + QVERIFY(tip->isVisible()); + + QRect trigGlobal(m_btnMid->mapToGlobal(QPoint(0,0)), m_btnMid->size()); + int trigCenterX = trigGlobal.center().x(); + int arrowGlobalX = tip->x() + tip->arrowLocalX(); + int delta = qAbs(arrowGlobalX - trigCenterX); + QVERIFY2(delta <= 2, + qPrintable(QStringLiteral("arrowGlobalX=%1 trigCenterX=%2 delta=%3") + .arg(arrowGlobalX).arg(trigCenterX).arg(delta))); + } + + // ── Anti-teleport ── + void testNoTeleportSameWidget() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Stable"); + QPoint pos1 = tip->pos(); + showAndProcess(m_btnMid, "Stable"); + QCOMPARE(tip->pos(), pos1); + } + + // ── Repositions for different widget ── + void testRepositionsForDifferentWidget() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnLeft, "Left"); + QPoint pos1 = tip->pos(); + showAndProcess(m_btnRight, "Right"); + QVERIFY2(tip->pos() != pos1, "Tooltip should move when trigger widget changes"); + } + + // ── Horizontal clamping ── + void testHorizontalClampLeft() { + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + QPoint oldPos = m_window->pos(); + m_window->move(avail.left(), avail.center().y() - 300); + QCoreApplication::processEvents(); + + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnLeft, "Clamped left"); + QVERIFY(tip->isVisible()); + QVERIFY2(tip->x() >= avail.left(), + qPrintable(QStringLiteral("tipX=%1 screenLeft=%2") + .arg(tip->x()).arg(avail.left()))); + + m_window->move(oldPos); + QCoreApplication::processEvents(); + } + + void testHorizontalClampRight() { + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + QPoint oldPos = m_window->pos(); + m_window->move(avail.right() - m_window->width(), avail.center().y() - 300); + QCoreApplication::processEvents(); + + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnRight, "Clamped right"); + QVERIFY(tip->isVisible()); + QVERIFY2(tip->x() + tip->width() <= avail.right() + 2, + qPrintable(QStringLiteral("tipRight=%1 screenRight=%2") + .arg(tip->x() + tip->width()).arg(avail.right()))); + + m_window->move(oldPos); + QCoreApplication::processEvents(); + } + + // ── Body rect dimensions ── + void testBodyRectSanity() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Body"); + QVERIFY(tip->isVisible()); + + QRect body = tip->bodyRect(); + QVERIFY(body.width() > 0); + QVERIFY(body.height() > 0); + QCOMPARE(tip->height(), body.height() + RcxTooltip::kArrowH); + } + + // ── Constants ── + void testConstants() { + QCOMPARE(RcxTooltip::kArrowH, 6); + QCOMPARE(RcxTooltip::kArrowHalfW, 6); + QCOMPARE(RcxTooltip::kGap, 2); + } + + // ────────────────────────────────────────────────────────────── + // RENDERING VERIFICATION — catches invisible tooltip bugs + // ────────────────────────────────────────────────────────────── + + void testShowForRendersBodyPixels() { + // Show tooltip and grab its rendered pixels. + // Verify that the body area has non-transparent content. + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Render test"); + QVERIFY(tip->isVisible()); + + // Force full opacity so grab gets real pixels + tip->setWindowOpacity(1.0); + QCoreApplication::processEvents(); + + QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32); + QVERIFY2(!img.isNull(), "grab() returned null image"); + QVERIFY2(img.width() > 0 && img.height() > 0, "grab() returned empty image"); + + // Check body rect area for opaque pixels + QRect body = tip->bodyRect(); + // Inset by 2px to avoid anti-aliased border edges + QRect checkRect = body.adjusted(2, 2, -2, -2); + int opaquePixels = countOpaquePixels(img, checkRect); + int totalPixels = checkRect.width() * checkRect.height(); + + QVERIFY2(opaquePixels > totalPixels / 2, + qPrintable(QStringLiteral( + "Body area has too few opaque pixels: %1 / %2 (< 50%%). " + "The tooltip is not rendering its background.") + .arg(opaquePixels).arg(totalPixels))); + } + + void testArrowRendersPixels() { + // Verify the triangle arrow region has some opaque pixels. + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Arrow test"); + QVERIFY(tip->isVisible()); + QVERIFY(tip->arrowPointsDown()); + + tip->setWindowOpacity(1.0); + QCoreApplication::processEvents(); + + QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32); + + // Arrow region: below the body rect, centered on arrowLocalX + QRect body = tip->bodyRect(); + int arrowTop = body.bottom(); + int arrowLeft = tip->arrowLocalX() - RcxTooltip::kArrowHalfW; + int arrowRight = tip->arrowLocalX() + RcxTooltip::kArrowHalfW; + QRect arrowRect(arrowLeft, arrowTop, arrowRight - arrowLeft, RcxTooltip::kArrowH); + + int opaquePixels = countOpaquePixels(img, arrowRect); + QVERIFY2(opaquePixels > 0, + qPrintable(QStringLiteral( + "Arrow region has 0 opaque pixels — triangle not painted. " + "arrowRect=(%1,%2 %3x%4) imgSize=(%5x%6)") + .arg(arrowRect.x()).arg(arrowRect.y()) + .arg(arrowRect.width()).arg(arrowRect.height()) + .arg(img.width()).arg(img.height()))); + } + + // ────────────────────────────────────────────────────────────── + // LEAVE EVENT RESILIENCE — catches spurious dismiss bugs + // ────────────────────────────────────────────────────────────── + + void testSurvivesLeaveEvent() { + // The tooltip should NOT be dismissed when a Leave event fires + // on the trigger widget while the cursor is still in the + // trigger+tooltip zone (simulates the synthetic Leave that Qt + // sends when a tooltip window pops up above the trigger). + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Survive Leave"); + QVERIFY(tip->isVisible()); + + tip->setWindowOpacity(1.0); + + // Move real cursor to center of trigger (so geometry check passes) + QPoint trigCenter = m_btnMid->mapToGlobal( + QPoint(m_btnMid->width() / 2, m_btnMid->height() / 2)); + QCursor::setPos(trigCenter); + QCoreApplication::processEvents(); + + // Send a Leave event to the trigger (like DarkApp::notify would) + QEvent leaveEvent(QEvent::Leave); + QApplication::sendEvent(m_btnMid, &leaveEvent); + + // Now call scheduleDismiss as DarkApp would + tip->scheduleDismiss(); + QCoreApplication::processEvents(); + + // Tooltip should STILL be visible — cursor is inside trigger zone + QVERIFY2(tip->isVisible(), + "Tooltip was dismissed by spurious Leave event while cursor " + "was still over the trigger widget"); + + // Wait beyond the dismiss timer to be sure + QTest::qWait(200); + QCoreApplication::processEvents(); + QVERIFY2(tip->isVisible(), + "Tooltip was dismissed after 200ms despite cursor being over trigger"); + } + + void testDismissesOnRealLeave() { + // When the cursor truly leaves the trigger+tooltip zone, + // scheduleDismiss() should queue dismissal and it should fire. + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Real leave"); + QVERIFY(tip->isVisible()); + + tip->setWindowOpacity(1.0); + + // Move cursor far away from both trigger and tooltip + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + QCursor::setPos(avail.bottomRight() - QPoint(10, 10)); + QCoreApplication::processEvents(); + + // scheduleDismiss should detect cursor is outside zone + tip->scheduleDismiss(); + QCoreApplication::processEvents(); + + // Wait for the 100ms dismiss timer + QTest::qWait(200); + QCoreApplication::processEvents(); + + QVERIFY2(!tip->isVisible(), + "Tooltip should have been dismissed when cursor left the zone"); + } + + void testLeaveAndReshow() { + // Dismiss via real leave, then re-show on a different widget. + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "First"); + QVERIFY(tip->isVisible()); + + // Force dismiss + tip->dismiss(); + QCoreApplication::processEvents(); + QVERIFY(!tip->isVisible()); + + // Re-show on different widget + showAndProcess(m_btnLeft, "Second"); + QVERIFY2(tip->isVisible(), "Tooltip failed to re-appear after dismiss"); + QCOMPARE(tip->currentText(), QString("Second")); + QCOMPARE(tip->currentTrigger(), m_btnLeft); + } + + // ── Scheduled dismiss cancelled by new showFor ── + void testScheduledDismissCancelledByShow() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "First"); + + // Move cursor far away and schedule dismiss + QScreen* scr = QApplication::primaryScreen(); + QCursor::setPos(scr->availableGeometry().bottomRight() - QPoint(10, 10)); + QCoreApplication::processEvents(); + tip->scheduleDismiss(); + + // Before timer fires, show on a different widget + showAndProcess(m_btnLeft, "Second"); + QTest::qWait(200); + QCoreApplication::processEvents(); + + // Should still be visible — new showFor cancelled the timer + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Second")); + } + + // ── Text change on same widget ── + void testTextChangeOnSameWidget() { + auto* tip = RcxTooltip::instance(); + showAndProcess(m_btnMid, "Text A"); + QCOMPARE(tip->currentText(), QString("Text A")); + + tip->dismiss(); + showAndProcess(m_btnMid, "Text B"); + QCOMPARE(tip->currentText(), QString("Text B")); + } +}; + +QTEST_MAIN(TestTooltip) +#include "test_tooltip.moc" diff --git a/tests/test_tooltip_event.cpp b/tests/test_tooltip_event.cpp new file mode 100644 index 0000000..3b2eb5d --- /dev/null +++ b/tests/test_tooltip_event.cpp @@ -0,0 +1,292 @@ +// Tests the full tooltip flow including DarkApp-style ToolTip interception. +// Verifies that QEvent::ToolTip fires and our custom tooltip appears. + +#include +#include +#include +#include +#include +#include +#include "rcxtooltip.h" +#include "themes/thememanager.h" +#include + +using namespace rcx; + +static void LOG(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + va_end(ap); + fflush(stdout); +} + +// Simulates DarkApp::notify behavior — installed as a global event filter +class DarkAppSimulator : public QObject { +public: + int tooltipEventCount = 0; + int leaveEventCount = 0; + int showForCallCount = 0; + + bool eventFilter(QObject* obj, QEvent* ev) override { + if (ev->type() == QEvent::ToolTip) { + tooltipEventCount++; + if (obj->isWidgetType()) { + auto* w = static_cast(obj); + QString tip = w->toolTip(); + LOG(" [darkapp-sim] ToolTip #%d on '%s' tip='%s'\n", + tooltipEventCount, qPrintable(w->objectName()), + qPrintable(tip.left(60))); + if (!tip.isEmpty()) { + showForCallCount++; + LOG(" [darkapp-sim] calling showFor #%d\n", showForCallCount); + RcxTooltip::instance()->showFor(w, tip); + LOG(" [darkapp-sim] after showFor: visible=%d pos=(%d,%d) size=%dx%d\n", + RcxTooltip::instance()->isVisible(), + RcxTooltip::instance()->x(), RcxTooltip::instance()->y(), + RcxTooltip::instance()->width(), RcxTooltip::instance()->height()); + return true; // consume — same as DarkApp + } + } + return true; // suppress default QToolTip + } + if (ev->type() == QEvent::Leave && obj->isWidgetType()) { + auto* tip = RcxTooltip::instance(); + if (tip->isVisible() && tip->currentTrigger() == obj) { + leaveEventCount++; + LOG(" [darkapp-sim] Leave #%d on trigger\n", leaveEventCount); + tip->scheduleDismiss(); + } + } + return false; + } +}; + +class TestTooltipEvent : public QObject { + Q_OBJECT + +private: + QWidget* m_window = nullptr; + QPushButton* m_btn = nullptr; + QPushButton* m_btn2 = nullptr; + DarkAppSimulator* m_sim = nullptr; + +private slots: + void initTestCase() { + LOG("=== TestTooltipEvent starting ===\n"); + + m_window = new QWidget; + m_window->setFixedSize(400, 300); + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + m_window->move(avail.center() - QPoint(200, 150)); + + m_btn = new QPushButton("Scan", m_window); + m_btn->setToolTip("Start scanning memory"); + m_btn->setFixedSize(120, 40); + m_btn->move(30, 130); + m_btn->setObjectName("btnScan"); + + m_btn2 = new QPushButton("Copy", m_window); + m_btn2->setToolTip("Copy to clipboard"); + m_btn2->setFixedSize(120, 40); + m_btn2->move(250, 130); + m_btn2->setObjectName("btnCopy"); + + // Install DarkApp simulator as global event filter + m_sim = new DarkAppSimulator; + qApp->installEventFilter(m_sim); + + m_window->show(); + m_window->activateWindow(); + m_window->raise(); + QVERIFY(QTest::qWaitForWindowExposed(m_window)); + // Let window become active + QTest::qWait(200); + QCoreApplication::processEvents(); + + LOG(" window at (%d,%d)\n", m_window->x(), m_window->y()); + LOG(" btn global: (%d,%d)\n", + m_btn->mapToGlobal(QPoint(60, 20)).x(), + m_btn->mapToGlobal(QPoint(60, 20)).y()); + } + + void cleanupTestCase() { + qApp->removeEventFilter(m_sim); + RcxTooltip::instance()->dismiss(); + delete m_sim; + delete m_window; + LOG("=== TestTooltipEvent finished ===\n"); + } + + void cleanup() { + RcxTooltip::instance()->dismiss(); + QCoreApplication::processEvents(); + m_sim->tooltipEventCount = 0; + m_sim->leaveEventCount = 0; + m_sim->showForCallCount = 0; + } + + // Test 1: Post QHelpEvent → DarkApp simulator intercepts → RcxTooltip shows + void testManualEventShowsTooltip() { + LOG("\n--- testManualEventShowsTooltip ---\n"); + auto* tip = RcxTooltip::instance(); + + QPoint btnGlobal = m_btn->mapToGlobal(QPoint(60, 20)); + QCursor::setPos(btnGlobal); + QCoreApplication::processEvents(); + + LOG(" posting QHelpEvent\n"); + QHelpEvent helpEvent(QEvent::ToolTip, QPoint(60, 20), btnGlobal); + QApplication::sendEvent(m_btn, &helpEvent); + QCoreApplication::processEvents(); + QTest::qWait(100); + QCoreApplication::processEvents(); + + LOG(" sim: tooltipEvents=%d showForCalls=%d\n", + m_sim->tooltipEventCount, m_sim->showForCallCount); + LOG(" tip: visible=%d text='%s'\n", + tip->isVisible(), qPrintable(tip->currentText())); + + QVERIFY2(m_sim->tooltipEventCount > 0, "Event filter didn't see ToolTip event"); + QVERIFY2(m_sim->showForCallCount > 0, "showFor was never called"); + QVERIFY2(tip->isVisible(), "RcxTooltip not visible after manual event"); + QCOMPARE(tip->currentText(), QString("Start scanning memory")); + + // Verify pixels + tip->setWindowOpacity(1.0); + QCoreApplication::processEvents(); + QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32); + QRect body = tip->bodyRect().adjusted(2, 2, -2, -2); + int opaque = 0; + for (int y = body.top(); y <= body.bottom(); ++y) + for (int x = body.left(); x <= body.right(); ++x) + if (qAlpha(img.pixel(x, y)) > 0) opaque++; + LOG(" pixels: %d/%d opaque\n", opaque, body.width() * body.height()); + QVERIFY2(opaque > body.width() * body.height() / 2, "Body not rendered"); + + LOG("--- testManualEventShowsTooltip PASSED ---\n"); + } + + // Test 2: Qt's native tooltip timer fires → our filter intercepts → tooltip shows + void testNativeTimerShowsTooltip() { + LOG("\n--- testNativeTimerShowsTooltip ---\n"); + auto* tip = RcxTooltip::instance(); + + // Move cursor away first + QPoint away = m_window->mapToGlobal(QPoint(380, 10)); + QCursor::setPos(away); + QTest::qWait(200); + QCoreApplication::processEvents(); + + // Move to button + QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20)); + LOG(" moving cursor to (%d,%d)\n", btnCenter.x(), btnCenter.y()); + QCursor::setPos(btnCenter); + + // Send Enter + MouseMove to kick the tooltip timer + QEvent enterEv(QEvent::Enter); + QApplication::sendEvent(m_btn, &enterEv); + QMouseEvent moveEv(QEvent::MouseMove, QPointF(60, 20), + m_btn->mapToGlobal(QPointF(60, 20)), + Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QApplication::sendEvent(m_btn, &moveEv); + + // Wait up to 2000ms for tooltip to appear + LOG(" waiting for Qt tooltip timer...\n"); + bool appeared = false; + for (int i = 0; i < 20; i++) { + QTest::qWait(100); + QCoreApplication::processEvents(); + if (m_sim->tooltipEventCount > 0) { + LOG(" tooltip event at ~%dms! events=%d showFor=%d\n", + (i+1)*100, m_sim->tooltipEventCount, m_sim->showForCallCount); + appeared = true; + break; + } + } + + // Process remaining events + QTest::qWait(100); + QCoreApplication::processEvents(); + + LOG(" final: events=%d showFor=%d visible=%d text='%s'\n", + m_sim->tooltipEventCount, m_sim->showForCallCount, + tip->isVisible(), qPrintable(tip->currentText())); + + QVERIFY2(appeared, "Qt tooltip timer never fired (no ToolTip event in 2 seconds)"); + QVERIFY2(tip->isVisible(), "Tooltip not visible after native timer fired"); + + LOG("--- testNativeTimerShowsTooltip PASSED ---\n"); + } + + // Test 3: Leave after tooltip shown → tooltip survives (cursor still in zone) + void testLeaveSurvival() { + LOG("\n--- testLeaveSurvival ---\n"); + auto* tip = RcxTooltip::instance(); + + QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20)); + QCursor::setPos(btnCenter); + QCoreApplication::processEvents(); + + // Show via manual event + QHelpEvent helpEvent(QEvent::ToolTip, QPoint(60, 20), btnCenter); + QApplication::sendEvent(m_btn, &helpEvent); + QCoreApplication::processEvents(); + QTest::qWait(100); + QCoreApplication::processEvents(); + QVERIFY(tip->isVisible()); + + // Send Leave (cursor still on button) + LOG(" sending Leave while cursor on button\n"); + QEvent leaveEv(QEvent::Leave); + QApplication::sendEvent(m_btn, &leaveEv); + QTest::qWait(200); + QCoreApplication::processEvents(); + + LOG(" after Leave+200ms: visible=%d leaves=%d\n", + tip->isVisible(), m_sim->leaveEventCount); + QVERIFY2(tip->isVisible(), "Tooltip dismissed by spurious Leave"); + + LOG("--- testLeaveSurvival PASSED ---\n"); + } + + // Test 4: Switch between widgets + void testWidgetSwitch() { + LOG("\n--- testWidgetSwitch ---\n"); + auto* tip = RcxTooltip::instance(); + + // Show on btn1 + QPoint btn1Center = m_btn->mapToGlobal(QPoint(60, 20)); + QCursor::setPos(btn1Center); + QCoreApplication::processEvents(); + QHelpEvent ev1(QEvent::ToolTip, QPoint(60, 20), btn1Center); + QApplication::sendEvent(m_btn, &ev1); + QCoreApplication::processEvents(); + QTest::qWait(100); + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Start scanning memory")); + QPoint pos1 = tip->pos(); + + // Switch to btn2 + QPoint btn2Center = m_btn2->mapToGlobal(QPoint(60, 20)); + QCursor::setPos(btn2Center); + QCoreApplication::processEvents(); + QHelpEvent ev2(QEvent::ToolTip, QPoint(60, 20), btn2Center); + QApplication::sendEvent(m_btn2, &ev2); + QCoreApplication::processEvents(); + QTest::qWait(100); + + LOG(" after switch: visible=%d text='%s' pos=(%d,%d)\n", + tip->isVisible(), qPrintable(tip->currentText()), + tip->x(), tip->y()); + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Copy to clipboard")); + QVERIFY(tip->pos() != pos1); + + LOG("--- testWidgetSwitch PASSED ---\n"); + } +}; + +QTEST_MAIN(TestTooltipEvent) +#include "test_tooltip_event.moc" diff --git a/tests/test_tooltip_ui.cpp b/tests/test_tooltip_ui.cpp new file mode 100644 index 0000000..41a2e9c --- /dev/null +++ b/tests/test_tooltip_ui.cpp @@ -0,0 +1,253 @@ +// Integration test: simulates the full tooltip flow as DarkApp would see it. +// Posts QHelpEvent (ToolTip), sends Leave events, verifies RcxTooltip behavior +// with fprintf at every stage so we can see exactly what happens. + +#include +#include +#include +#include +#include +#include +#include "rcxtooltip.h" +#include "themes/thememanager.h" +#include + +using namespace rcx; + +static void LOG(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + va_end(ap); + fflush(stdout); +} + +// Simulates what DarkApp::notify does when a ToolTip event arrives +static bool simulateDarkAppToolTip(QWidget* w) { + QString tip = w->toolTip(); + LOG(" [darkapp] widget='%s' class=%s tip='%s'\n", + qPrintable(w->objectName()), w->metaObject()->className(), + qPrintable(tip)); + if (!tip.isEmpty()) { + LOG(" [darkapp] calling RcxTooltip::showFor\n"); + RcxTooltip::instance()->showFor(w, tip); + LOG(" [darkapp] showFor returned, visible=%d opacity=%.2f pos=(%d,%d) size=%dx%d\n", + RcxTooltip::instance()->isVisible(), + RcxTooltip::instance()->windowOpacity(), + RcxTooltip::instance()->x(), RcxTooltip::instance()->y(), + RcxTooltip::instance()->width(), RcxTooltip::instance()->height()); + return true; + } + return false; +} + +// Simulates what DarkApp::notify does when a Leave event arrives +static void simulateDarkAppLeave(QWidget* w) { + auto* tip = RcxTooltip::instance(); + if (tip->isVisible() && tip->currentTrigger() == w) { + LOG(" [darkapp] Leave on trigger — calling scheduleDismiss\n"); + tip->scheduleDismiss(); + LOG(" [darkapp] after scheduleDismiss: visible=%d\n", tip->isVisible()); + } else { + LOG(" [darkapp] Leave ignored (visible=%d trigger_match=%d)\n", + tip->isVisible(), tip->currentTrigger() == w); + } +} + +class TestTooltipUI : public QObject { + Q_OBJECT + +private: + QWidget* m_window = nullptr; + QPushButton* m_btn = nullptr; + QPushButton* m_btn2 = nullptr; + +private slots: + void initTestCase() { + LOG("=== TestTooltipUI starting ===\n"); + + m_window = new QWidget; + m_window->setFixedSize(400, 300); + QScreen* scr = QApplication::primaryScreen(); + QRect avail = scr->availableGeometry(); + m_window->move(avail.center() - QPoint(200, 150)); + + m_btn = new QPushButton("Scan", m_window); + m_btn->setToolTip("Start scanning memory"); + m_btn->setFixedSize(80, 28); + m_btn->move(160, 140); + m_btn->setObjectName("btnScan"); + + m_btn2 = new QPushButton("Copy", m_window); + m_btn2->setToolTip("Copy address to clipboard"); + m_btn2->setFixedSize(80, 28); + m_btn2->move(260, 140); + m_btn2->setObjectName("btnCopy"); + + m_window->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_window)); + LOG(" window shown at (%d,%d)\n", m_window->x(), m_window->y()); + } + + void cleanupTestCase() { + RcxTooltip::instance()->dismiss(); + delete m_window; + LOG("=== TestTooltipUI finished ===\n"); + } + + void cleanup() { + RcxTooltip::instance()->dismiss(); + QCoreApplication::processEvents(); + } + + // ─── Test 1: Full tooltip lifecycle with event simulation ─── + void testFullLifecycle() { + LOG("\n--- testFullLifecycle ---\n"); + auto* tip = RcxTooltip::instance(); + + // Step 1: Post a ToolTip event (what Qt does after hover delay) + LOG("Step 1: Posting ToolTip event to btn\n"); + QPoint btnCenter = m_btn->mapToGlobal(QPoint(40, 14)); + LOG(" btn global center: (%d,%d)\n", btnCenter.x(), btnCenter.y()); + + // Move real cursor to button center + QCursor::setPos(btnCenter); + QCoreApplication::processEvents(); + LOG(" cursor moved to button\n"); + + // Simulate what DarkApp does on ToolTip event + bool handled = simulateDarkAppToolTip(m_btn); + QVERIFY2(handled, "DarkApp should have handled the tooltip"); + + // Process events (paint, animation start) + QCoreApplication::processEvents(); + QTest::qWait(100); // let fade-in animation run + QCoreApplication::processEvents(); + + LOG("Step 2: Check tooltip state after 100ms\n"); + LOG(" visible=%d opacity=%.2f text='%s'\n", + tip->isVisible(), tip->windowOpacity(), + qPrintable(tip->currentText())); + LOG(" pos=(%d,%d) size=%dx%d\n", + tip->x(), tip->y(), tip->width(), tip->height()); + LOG(" arrowDown=%d arrowX=%d bodyRect=(%d,%d %dx%d)\n", + tip->arrowPointsDown(), tip->arrowLocalX(), + tip->bodyRect().x(), tip->bodyRect().y(), + tip->bodyRect().width(), tip->bodyRect().height()); + + QVERIFY2(tip->isVisible(), "Tooltip should be visible after showFor + 100ms"); + QCOMPARE(tip->currentText(), QString("Start scanning memory")); + + // Step 3: Grab pixels and verify rendering + LOG("Step 3: Verify rendering\n"); + tip->setWindowOpacity(1.0); + QCoreApplication::processEvents(); + + QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32); + LOG(" grabbed image: %dx%d format=%d\n", img.width(), img.height(), img.format()); + + int opaquePixels = 0; + QRect body = tip->bodyRect().adjusted(2, 2, -2, -2); + for (int y = body.top(); y <= body.bottom(); ++y) + for (int x = body.left(); x <= body.right(); ++x) + if (qAlpha(img.pixel(x, y)) > 0) + ++opaquePixels; + int totalPixels = body.width() * body.height(); + LOG(" body opaque pixels: %d / %d (%.1f%%)\n", + opaquePixels, totalPixels, + totalPixels > 0 ? 100.0 * opaquePixels / totalPixels : 0.0); + + QVERIFY2(opaquePixels > totalPixels / 2, + qPrintable(QStringLiteral("Only %1/%2 opaque pixels in body — tooltip not rendering") + .arg(opaquePixels).arg(totalPixels))); + + // Step 4: Simulate Leave event (spurious — cursor still on button) + LOG("Step 4: Simulate spurious Leave (cursor still on button)\n"); + simulateDarkAppLeave(m_btn); + QTest::qWait(200); + QCoreApplication::processEvents(); + LOG(" after 200ms: visible=%d\n", tip->isVisible()); + + QVERIFY2(tip->isVisible(), + "Tooltip dismissed by spurious Leave — geometry check failed"); + + // Step 5: Move cursor away and simulate real Leave + LOG("Step 5: Move cursor away, simulate real Leave\n"); + QScreen* scr = QApplication::primaryScreen(); + QPoint farAway = scr->availableGeometry().bottomRight() - QPoint(50, 50); + QCursor::setPos(farAway); + QCoreApplication::processEvents(); + LOG(" cursor at (%d,%d)\n", farAway.x(), farAway.y()); + + simulateDarkAppLeave(m_btn); + QTest::qWait(200); + QCoreApplication::processEvents(); + LOG(" after 200ms: visible=%d\n", tip->isVisible()); + + QVERIFY2(!tip->isVisible(), + "Tooltip should be dismissed when cursor truly left the zone"); + + // Step 6: Re-show on different widget + LOG("Step 6: Re-show on different widget\n"); + QPoint btn2Center = m_btn2->mapToGlobal(QPoint(40, 14)); + QCursor::setPos(btn2Center); + QCoreApplication::processEvents(); + + handled = simulateDarkAppToolTip(m_btn2); + QVERIFY(handled); + QCoreApplication::processEvents(); + QTest::qWait(100); + QCoreApplication::processEvents(); + + LOG(" visible=%d text='%s'\n", tip->isVisible(), qPrintable(tip->currentText())); + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Copy address to clipboard")); + + LOG("--- testFullLifecycle PASSED ---\n"); + } + + // ─── Test 2: Rapid widget switching (no dismiss between) ─── + void testRapidSwitch() { + LOG("\n--- testRapidSwitch ---\n"); + auto* tip = RcxTooltip::instance(); + + QCursor::setPos(m_btn->mapToGlobal(QPoint(40, 14))); + QCoreApplication::processEvents(); + simulateDarkAppToolTip(m_btn); + QCoreApplication::processEvents(); + QTest::qWait(50); + + LOG(" switch to btn2 immediately\n"); + QCursor::setPos(m_btn2->mapToGlobal(QPoint(40, 14))); + QCoreApplication::processEvents(); + simulateDarkAppToolTip(m_btn2); + QCoreApplication::processEvents(); + QTest::qWait(100); + QCoreApplication::processEvents(); + + LOG(" visible=%d text='%s'\n", tip->isVisible(), qPrintable(tip->currentText())); + QVERIFY(tip->isVisible()); + QCOMPARE(tip->currentText(), QString("Copy address to clipboard")); + LOG("--- testRapidSwitch PASSED ---\n"); + } + + // ─── Test 3: Widget with no tooltip ─── + void testNoTooltipWidget() { + LOG("\n--- testNoTooltipWidget ---\n"); + QPushButton noTip("NoTip", m_window); + noTip.setFixedSize(80, 28); + noTip.move(50, 50); + noTip.show(); + // No setToolTip called + + auto* tip = RcxTooltip::instance(); + bool handled = simulateDarkAppToolTip(&noTip); + LOG(" handled=%d visible=%d\n", handled, tip->isVisible()); + QVERIFY(!handled); + QVERIFY(!tip->isVisible()); + LOG("--- testNoTooltipWidget PASSED ---\n"); + } +}; + +QTEST_MAIN(TestTooltipUI) +#include "test_tooltip_ui.moc"