Compare commits

..

133 Commits

Author SHA1 Message Date
IChooseYou
86499e58ee fix: remove value history cooldown hack, dismiss popup on clear
The cooldown suppressed tracking for ~1s but the popup persisted showing
stale "1h ago" values because applyDocument skips popup dismissal.
Replaced with explicit dismissHistoryPopup() after clear+refresh so the
popup is gone immediately. Value tracking resumes on the next async cycle
with a clean baseline (m_refreshGen++ discards in-flight reads,
m_prevPages.clear() prevents phantom diffs).
2026-03-03 08:38:08 -07:00
IChooseYou
b2ae8d5a5d fix: insert above node, clear value history cooldown, search context menu
- Insert 4/8 now inserts above the right-clicked node and shifts siblings
  down instead of appending at end. Insert key shortcut (Shift+Ins = 4,
  Ins = 8). Falls back to append when clicking empty space.
- Clear Value History uses a 5-cycle cooldown counter so heat stays gone
  for ~1s instead of returning on the next async refresh.
- Right-click Search defers showFindBar via QTimer::singleShot so focus
  isn't stolen by the closing context menu.
2026-03-03 08:31:49 -07:00
IChooseYou
6768f04e9a Merge pull request #6 from LabGuy94/add-macos-support
Fix file opening on macOS
2026-03-03 08:31:25 -07:00
Lab
c6e5f6508f Fix file opening on macOS 2026-03-02 15:25:57 -08:00
IChooseYou
e6529052b3 fix: clear value history clears subtree, add Copy Line and Search to context menu
- Clear Value History now removes history for all descendant nodes too
- Add "Copy Line" right-click menu item
- Add "Search..." right-click menu item (opens Ctrl+F find bar)
- Move showFindBar() to public in editor.h
2026-03-02 15:34:37 -07:00
IChooseYou
d43e989992 Merge pull request #5 from LabGuy94/add-macos-support
Add macOS support and CI
2026-03-02 14:57:55 -07:00
IChooseYou
879e9f4047 fix: global blue highlight, Ctrl+F find bar with prev/next/close buttons
- Change QPalette::Highlight from theme.selection to theme.hover globally
- RcxEditor find: use SCI_SEARCHINTARGET + INDIC_COMPOSITIONTHICK indicator
  (selection rendering is disabled, so findFirst was invisible)
- Re-apply find indicators after applyDocument() refresh cycle
- Add prev/next/close buttons to find bars in both Reclass and C/C++ modes
- Buttons styled with hover/pressed states matching tab styling
2026-03-02 14:53:14 -07:00
Lab
e0d5a799b4 Add macOS support and CI 2026-03-02 11:34:22 -08:00
IChooseYou
efae193520 feat: value history timestamps, Ctrl+F search, base address fixes
- Add timestamps to ValueHistory ring buffer, expose via new MCP tool
  node.history, show relative time in popup ("26s ago", "2m ago")
- Add "Clear Value History" right-click menu for single and multi-select
- Add Ctrl+F find bar to RcxEditor with live search, Enter-to-next, wrap
- Fix Ctrl+F in workspace dock to auto-focus search field
- Add "Change to float" quick-convert for Hex32 right-click menu
- Sort workspace explorer by children count descending (most fields first)
- Fix provider->base() overwriting saved base address from .rcx files
- Add formula support to MCP change_base operation
- Re-evaluate baseAddressFormula on provider attach in selectSource()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:00:17 -07:00
IChooseYou
ba1c2f8e5a refactor: process picker themed styling, context menu, auto-select
Extract shared init into initUi(). Apply dark theme styling from global
palette to table, header, filter, and buttons. Add right-click context
menu with Copy PID/Name/Path. Auto-select last attached process on open.
Remove duplicate attach->accept() connection from .ui (handled in code).
2026-03-02 08:24:39 -07:00
IChooseYou
5a0a4d1802 feat: recent files menu, remove split visibility, clean up demo data
Add Recent Files submenu under File menu (persists last 10 opened/saved
files in QSettings). Hide Remove Split action until a split actually
exists. Remove _SAMPLE_OBJECT demo class from both buildEmptyStruct and
buildEditorDemo. Create a second empty class tab on selfTest so the user
starts with a clean workspace.
2026-03-02 07:50:46 -07:00
Sen66
030eb34510 fix: include shim also on linux 2026-03-02 00:11:37 +01:00
Sen66
2939b25895 fix: build instructions for fadec on cmake build 2026-03-02 00:08:11 +01:00
Sen66
d38cb02fa2 fix: mingw build 2026-03-01 23:58:06 +01:00
IChooseYOu
9f285b37b2 Merge remote-tracking branch 'origin/msvc' 2026-03-01 14:03:34 -07:00
IChooseYOu
cae599a0c6 fix: fixed-width float formatting, fix test_32bit_support on Linux CI
Float values now use a fixed 7-char body (digits.decimals + f suffix)
that adapts decimal places to the integer magnitude. Removes the
variable-width 'g' format and sign-space prefix.

Set QT_QPA_PLATFORM=offscreen for test_32bit_support so it no longer
crashes on headless Linux CI without an X display.
2026-03-01 14:02:40 -07:00
Sen66
d0734ba8be update readme with MSVC/VS guide 2026-03-01 22:02:07 +01:00
Sen66
696ff044ac msvc VS22+ support 2026-03-01 21:54:17 +01:00
Sen66
da312ccac6 fix app close crash, fix error on msvc 2026-03-01 21:49:49 +01:00
Sen66
552b45b16c Fix QSci assert on msvc with text being nullptr 2026-03-01 21:40:57 +01:00
Sen66
e89fd4a6c1 make fadec, raw_pdb submodule 2026-03-01 18:55:26 +01:00
Sen66
7524004b32 remove /fadec and /raw_pdb content 2026-03-01 18:46:01 +01:00
IChooseYou
ed8a44917b feat: 32-bit process support, scanner rescan filtering, suppress flash on navigate
- Add pointerSize() to Provider base; WoW64/ELF detection in ProcessMemory,
  WinDbg, and RemoteProcessMemory plugins
- Wire pointer size through NodeTree, source/XML imports, C++ generator,
  controller, compose, address parser, and RPC protocol header
- Add is32Bit to PluginProcessInfo and ProcessInfo; show (32-bit) in picker
- Scanner rescan now filters results against the current input value
- Go-to-address from scanner resets change tracking to prevent false flashing
2026-03-01 07:42:40 -07:00
IChooseYou
ecfac3decf fix: add missing test source files to repository 2026-02-28 12:54:38 -07:00
IChooseYou
851d744263 fix: rescan performance overhaul, background thread, WinDbg regions
Move rescan to background thread via ScanEngine::startRescan() to
prevent UI freeze. Fix populateTable bottleneck caused by
QHeaderView::ResizeToContents iterating all rows (6s -> 0ms for 512
results). Add chunked batch reads (256KB spans), enumerateRegions()
for WinDbg/ProcessMemory providers, cancel support, and diagnostic
logging throughout the scanner pipeline.
2026-02-28 12:53:25 -07:00
IChooseYou
41e2f9f662 feat: scanner panel with signature/value search, rescan, address delegate
- Signature mode (IDA-style patterns with wildcards) and value mode (typed exact match)
- Async scan engine with progress, cancel support
- Re-scan updates all results with unified progress (single-pass read + table build)
- Previous value column appears after first re-scan
- WinDbg backtick address format with dimmed leading zeros (AddressDelegate)
- Inline editing: address expressions navigate, value edits write to provider
- Right-click context menu: Copy Address, Copy Value, Go to Address
- Auto-sized columns, themed buttons with icons, dynamic combo width
- 49 UI tests covering scan, rescan, editing, theming, progress completion
2026-02-28 11:53:51 -07:00
IChooseYou
95faf027a9 refactor: rename helpers to static fields, block-style rendering, sibling insert
Rename isHelper/ToggleHelper to isStatic/ToggleStatic across core, compose,
controller, editor, and generator. Static fields now render with block syntax
(static Type name { return expr } → 0xADDR) and support collapsed/expanded
display. Add "Add Static Field" context menu for sibling nodes. Update
expression span parser, completions, C++ generator comments, and all tests.
2026-02-28 08:21:00 -07:00
IChooseYou
6a51c904de feat: type selector overhaul, fuzzy search, address parser, value tracking
Redesign type selector popup with fuzzy subsequence matching, per-category
icons, field summary tooltips, compact chips, and pointer target primitives.
Add address expression parser with arithmetic and register support.
Enable track value changes by default.
2026-02-28 06:59:22 -07:00
IChooseYou
0d73575ea7 fix: C++ generator bitfields, sizeof placement, Ctrl+F search, view sync
- Generator emits proper bitfield members instead of padding stubs
- Named bitfield structs (MitigationFlagsValues etc) now converted by parser
- sizeof comment moved from top to closing brace (}; // sizeof 0x80)
- C/C++ view syncs with workspace double-click and controller navigation
- Ctrl+F incremental search in C++ code view (Enter=next, Escape=close)
- Workspace dock resizable via 1px drag handle separator
- Regenerated Vergilius_25H2.rcx with all fixes (61 named bitfield containers)
2026-02-26 12:07:55 -07:00
IChooseYou
aa04cfcb5c feat: add Vergilius-to-RCX converter, full Windows 11 25H2 kernel structs
Add tools/vergilius_to_rcx.py: scrapes struct definitions from
vergiliusproject.com and generates .rcx JSON files. Supports bitfields,
arrays, self-referential pointers, deep union/struct nesting, and
cross-struct references. Offsets correctly stored as parent-relative.

Add src/examples/Vergilius_25H2.rcx: 1,690 kernel structs (18,924 nodes)
from Windows 11 25H2 including _EPROCESS, _KTHREAD, _MMPFN, _PEB, etc.

Remove orange M_CYCLE background on self-referential pointer children —
rows now render with normal theme background while retaining click-to-
materialize behavior.
2026-02-26 11:02:12 -07:00
IChooseYou
1465e7fbed feat: Vergilius-style C++ generator, struct type click fix, item view highlight fix
Rewrite C++ generator for Vergilius-style output: inline anonymous
structs/unions, reference opaque types by name with struct keyword
prefix, size comments, aligned offset comments, no anon_ stubs.

Fix struct type name not clickable in editor headers (headerTypeNameSpan
assumed "struct TYPENAME" format but named structs use bare name).

Add static_assert toggle in Options > Generator, default off.

Fix item view highlight bleed: patch PE_PanelItemViewRow to use
theme.hover so row background matches CE_ItemViewItem.
2026-02-26 08:21:15 -07:00
IChooseYou
52f751e751 fix: redesign Type Aliases dialog — visible presets, compact layout
stdint button now fills cells with actual type names instead of clearing
to empty. Removed redundant Reset button, hidden column/row headers,
filtered out irrelevant types (Vec/Mat/Struct/Array). Fixed item view
hover being invisible on dark themes by painting explicit fillRect.
2026-02-25 17:39:17 -07:00
IChooseYou
0a19789a9d feat: enhance workspace dock, reorganize menus, fix Reclass Dark theme
- Workspace dock: show member count per type, expandable child rows
  (Type Name format, Hex padding filtered), search/filter box with
  recursive matching, collapsed by default, double-click navigates
  to member in editor
- Menu reorganization: Import/Export submenus, new Tools menu (Type
  Aliases, MCP Server, Options), Data Source moved to View, renamed
  Unload→Close Project, Unsplit→Remove Split, Current Tab Source→
  Data Source
- View menu: add Relative Offsets toggle (persisted, applies to all
  editors and new splits)
- Fix Reclass Dark theme: hover/selected colors were identical to
  background (#1e1e1e), now #2a2a2a/#2a2d2e for visible contrast
- Dim MDI tab text via QPalette::WindowText (Fusion ignores CSS color)
- Remove dead QProxyStyle tab handlers (never called for QMdiArea)
2026-02-25 14:27:02 -07:00
IChooseYou
62a68bef80 fix: align workspace dock header with MDI tab bar, dim tab text
Use QProxyStyle for tab height (24px) and text color instead of CSS.
Selected/hover tabs now use textDim to match the dock header.
2026-02-24 15:16:33 -07:00
IChooseYou
4941f860b6 docs: fix misleading README claims, add missing features, remove hr noise
- Fix "server does not start by default" (MCP now auto-starts)
- Rephrase tagline to name ReClass.NET/ReClassEx directly
- Add missing features: enums, bitfields, PDB import, themes, disasm preview, heatmap, MDI tabs, import/export
- Note Qt 5 support alongside Qt 6
- Align autoStartMcp default to true in options dialog
- Remove all horizontal rule separators
2026-02-24 12:48:50 -07:00
IChooseYou
c45d51d736 feat: shimmer status bar for MCP activity, auto-start MCP, remove "Ready" spam
- Add ShimmerLabel widget with animated glow band for MCP tool activity
- Separate app/MCP status channels (setAppStatus/setMcpStatus/clearMcpStatus)
- 750ms delayed clear so shimmer stays visible after fast tool calls
- MCP auto-starts on launch by default
- Remove "Ready" text that was overwriting useful status info
- Add statusText field to project.state MCP response
2026-02-24 12:31:25 -07:00
IChooseYou
5b46065403 feat: enum/bitfield editing, MCP guard rails, PDB anonymous type inlining
- Enum inline editing: name/value commit handling, auto-sort by value
- Bitfield support in PDB import with proper container nodes
- Per-member hover/selection highlighting (kMemberBit encoding)
- Context menu fixes for enum/bitfield member lines
- MCP pagination (limit/offset), includeMembers param, tree.search tool
- MCP status bar activity indicator for tool calls
- PDB anonymous type inlining: inline <unnamed-tag> types as children
- Skip anonymous pointer targets to prevent root orphans
- Enum import diagnostics for debugging missing enums
2026-02-24 10:37:42 -07:00
IChooseYou
4706f7b782 Merge branch 'docs' — update README and add banner SVGs 2026-02-23 18:33:41 -07:00
IChooseYou
fe9bfafa3b Merge pull request #3 from H4vC/main
perf: removed redundant cache invalidations and preindexed lookups for pdbs
2026-02-23 16:07:27 -07:00
IChooseYou
ff928df685 feat: enum support, workspace styling, EPROCESS/MMPFN test data
- Import enums from C/C++ source and PDB with name/value members
- Compose/format/generate enum definitions properly
- Workspace dock: rename to Project, theme-based titlebar and selection
- Add comprehensive EPROCESS.rcx (325 nodes) and MMPFN.rcx (65 nodes)
2026-02-23 16:01:35 -07:00
Brit
d6e3c182fc perf(import-compose): removed redundant cache invalidations and preindexed lookups 2026-02-23 17:56:44 +01:00
IChooseYou
078a6028f0 fix: WinDbg provider stops auto-selecting module, new tabs inherit source
- WinDbg provider no longer picks arbitrary module[0] as name/base
  (was showing "WS2_32" for kernel dumps). Name is now generic
  "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller
  doesn't override user's address.
- Added throttled read failure logging to WinDbg provider.
- New tabs (File→New Class, workspace right-click) inherit the
  current tab's source/provider so users don't have to re-attach.
- Updated WinDbg provider tests for new behavior.
2026-02-23 08:08:46 -07:00
Sen66
d7a6e1862e update height of banner 2026-02-22 22:02:27 +01:00
Sen66
1ddf47a754 update svg 2026-02-22 22:01:29 +01:00
Sen66
1a885a8b1d update readme 2026-02-22 21:54:11 +01:00
IChooseYou
67218d3e48 fix: move payload init out of DllMain to avoid loader lock deadlock
RcxPayloadInit() is now an exported function called after LoadLibrary
returns. DllMain only handles cleanup on detach. Timer queue creation
under the loader lock was crashing target processes.
2026-02-22 13:14:01 -07:00
IChooseYou
f651edd740 feat: remove nonce/bootstrap from remote process IPC, use PID-only naming
Shared memory names simplified to Local\RCX_SHM_<pid>, no bootstrap
handshake needed. Payload uses CreateTimerQueueTimer (10ms poll) instead
of a dedicated server thread.
2026-02-22 11:36:24 -07:00
IChooseYou
25aaace382 Merge remote-tracking branch 'origin/fix-issue-2' 2026-02-22 11:09:05 -07:00
Sen66
b5ddb042b8 Try to fix missing DLLs at CI windows builds
Fix https://github.com/IChooseYou/Reclass/issues/2
2026-02-22 19:06:50 +01:00
IChooseYou
e900dea836 fix: menu bar item paint no longer covers title bar bottom border
Take full ownership of CE_MenuBarItem in MenuBarStyle — never
delegate to Fusion which unconditionally fills the full item rect.
Non-hovered items draw text only (transparent bg lets parent border
show through). Hover/pressed states fill adjusted rect leaving 1px
for the border. Pressed state uses darker(130) for visual feedback.
2026-02-22 11:05:54 -07:00
IChooseYou
b647a334bc docs: fix Remote Process description 2026-02-22 09:14:04 -07:00
IChooseYou
fc390bc1f7 docs: add Remote Process data source to README 2026-02-22 09:06:32 -07:00
IChooseYou
7efe740ec1 fix: hover invisible when theme.hover == background, remove CSS on QMenuBar
Move hover color fixup into Theme::fromJson so all consumers get a
visible hover automatically. Remove duplicate lighter(130) fallback
from applyGlobalTheme. Replace QMenuBar CSS with QPalette so
MenuBarStyle QProxyStyle is not bypassed. Add PE_PanelMenuBar and
CE_MenuBarEmptyArea suppression so Fusion never paints over the
title bar background.
2026-02-22 08:58:57 -07:00
IChooseYou
48409d1d38 fix: guard __cdecl __debugbreak behind PDB_COMPILER_MSVC for Linux build
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:33:36 -07:00
IChooseYou
df1435d9b7 Merge remote-tracking branch 'origin/refactor-readme' 2026-02-22 07:30:00 -07:00
IChooseYou
5e11ff5496 feat: Remote Process Memory plugin, source menu icons, base address fix
- Remote Process Memory plugin: shared-memory IPC payload injected into
  target process (CreateRemoteThread on Win, ptrace+dlopen on Linux),
  VirtualQuery-based memory safety, PEB-based image base, batch reads
- Source dropdown: SVG icons per provider type, DLL filename shown
- Fix base address not updating when switching to a new source provider
- ProviderRegistry carries DLL filename from PluginManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:29:56 -07:00
Sen66
22842d9801 I'm too tired 2026-02-22 14:22:36 +01:00
Sen66
50acde60cb refactor readme 2026-02-22 14:21:12 +01:00
IChooseYou
1d7d384b93 feat: PDB import via RawPDB, no msdia140.dll dependency
Replace DIA SDK COM-based PDB importer with RawPDB (MolecularMatters)
which reads PDB files directly via memory-mapped I/O. Adds File menu
"Import PDB..." dialog with type filtering, selection, and progress.

- Vendor raw_pdb into third_party/
- Two-phase API: enumeratePdbTypes() + importPdbSelected()
- Full recursive import of structs/unions/arrays/pointers/bitfields
- PDB import dialog with name filter, select-all, type count
- Benchmark: 1654 types from ntkrnlmp.pdb in 16ms
- Reorganize import/export files into src/imports/
2026-02-21 17:18:24 -07:00
IChooseYou
3a76b03c85 fix: continuous top border on status bar tabs, baseline alignment, 15% taller
- ViewTabButton always paints 1px top border matching status bar hairline;
  selected tab's accent line paints over it
- Remove SegmentedContainer (caused gap on unselected tab)
- Shared baseline alignment between tab text and status label
- Status bar height * 1.15
2026-02-21 11:41:46 -07:00
IChooseYou
ac94855d6c feat: status bar visual upgrade, unified release job
Status bar: top hairline separator, vertical divider between toggle
and status text, segmented-control container with border/separators
around view buttons, accent line 2->3px, proper sizeHint with
breathing room, default system font instead of monospace override.

CI: replace per-job release uploads with a single release job that
waits for both windows and linux, then publishes both artifacts to
one GitHub release.
2026-02-21 11:09:28 -07:00
IChooseYou
d65b6c5a29 feat: address expression parser with module resolution and pointer deref
Merge branch 'address-parser'. Adds AddressParser supporting:
- Hex arithmetic with +-*/ and operator precedence
- Module base resolution via <Module.exe> syntax
- Pointer dereference via [addr] syntax with nesting
- WinDbg backtick-separated addresses (7ff6`6cce0000)
- Formula persistence in project files and source switching
2026-02-21 09:12:11 -07:00
IChooseYou
d45ee9e4c9 ci: install Qt-matching MinGW 13.1.0 to fix test segfaults
System MinGW on windows-latest is GCC 15.2 which has ABI mismatch
with Qt 6.8.1 (built with MinGW 13.1.0), causing all tests to
segfault. Install the matching toolchain via aqtinstall tools and
use it instead of the system compiler.
2026-02-21 09:07:09 -07:00
Sen66
31115014a5 ignore some more build directories 2026-02-21 17:04:59 +01:00
Sen66
8e88d588be Add AddressParser + tests, remove symbol from commandrow 2026-02-21 17:03:44 +01:00
IChooseYou
b089e20d36 ci: retrigger build 2026-02-20 17:01:57 -07:00
IChooseYou
5fa1dd0ab4 fix: add missing header declarations and editor scroll fixes
- mainwindow.h: add m_viewBtnGroup, m_btnReclass, m_btnRendered members,
  syncViewButtons() declaration, QButtonGroup/QPushButton includes,
  remove applyTabWidgetStyle() declaration
- editor.cpp: reset xOffset on applyDocument, clamp in restoreViewState
- test_editor.cpp: add horizontal scroll reset test
2026-02-20 13:22:23 -07:00
IChooseYou
3b1fe7ff35 fix: use findChild<QWidget*> for ResizeGrip to fix GCC 15 static_assert
GCC 15.2 on CI enforces Q_OBJECT requirement for findChild template
parameter. ResizeGrip is a local class without Q_OBJECT, so use
QWidget* with static_cast instead.
2026-02-20 13:18:03 -07:00
IChooseYou
4595b366e3 ci: use system MinGW from runner, drop tools_mingw1310 2026-02-20 13:05:13 -07:00
IChooseYou
33d7dc74cb ci: switch Windows CI from MSVC to MinGW, run Linux in parallel 2026-02-20 12:57:51 -07:00
IChooseYou
e118231bb1 docs: add screenshots to README 2026-02-20 12:32:04 -07:00
IChooseYou
0cfd7ad87a feat: sort primitives alphabetically in type chooser 2026-02-20 07:37:32 -07:00
IChooseYou
2d3ce63b54 ci: disable UI tests in CI, delete test_com_security
CI now passes -DBUILD_UI_TESTS=OFF so only headless tests
(core, format, compose, provider, command_row, generator,
import_xml, import_source, export_xml, disasm) build and run.

Removed xvfb-run and exclude-regex hacks from both Windows
and Linux CI — the CMake option handles it cleanly.

Deleted test_com_security (windbg-only, not needed in CI).
2026-02-20 07:27:23 -07:00
IChooseYou
0e087fa3a4 feat: primitive pointer modifiers, type chooser fixes, double-click to edit
Type chooser:
- Fix PointerTarget mode hiding primitives due to stale modifier state
- Preselect */[n] modifier buttons to reflect current node type
- Primitive pointer support: int32*, double**, etc with provider deref
- hex64*/ptr64* with * modifier falls back to void* (meaningless deref)
- isValidPrimitivePtrTarget guard in controller, compose, format
- Modifier toggle no longer resets list selection
- Primitive pointers open FieldType mode (not PointerTarget)
- Type edit requires double-click (was single-click, too easy to misclick)

Other:
- Custom dock titlebar with themed close button, no float button
- Status bar font synced at startup
- Resize grip reworked as direct MainWindow child, font-independent
- File menu "Source" renamed to "Current Tab Source"

Tests: 41 type_selector, 39 editor, 17 controller (200 total, 0 failures)
2026-02-20 07:21:02 -07:00
IChooseYou
c7afe363f3 feat: custom dock titlebar, resize grip symmetry fix, status bar font sync
- Replace default dock widget titlebar with custom label + themed ✕ close button
- Remove float/popout button from project tree dock
- Fix resize grip corner symmetry (bottom margin 4→0)
- Sync editor font to status bar and dock titlebar at startup
- Add testResizeGripCornerSymmetry test
2026-02-19 18:10:52 -07:00
IChooseYou
2a44d2ac57 fix: narrow inline editor selection for pointer values, resolve correct write address
resolvedSpanFor() now applies narrowPtrValueSpan() to trim the "// Module+offset"
symbol comment from the editable span, matching hitTestTarget(). Previously the
full value column text was selected, making the parser fail on commit (toULongLong
rejected the non-hex suffix), so pointer value saves were silently no-ops.

With the parse now succeeding, a second bug was exposed: setNodeValue() computed
write addresses via computeOffset() which sums tree offsets without dereferencing
pointers. For nodes inside expanded pointer targets (e.g. VTable entries), this
wrote to struct_base+child_offset instead of *ptr_value+child_offset, causing an
access violation crash. The fix passes lm->offsetAddr (the compose-resolved
absolute address) through the inlineEditCommitted signal so setNodeValue() uses
the correct dereferenced address.
2026-02-19 13:05:25 -07:00
IChooseYou
d989e2a947 feat: safe workspace tree deletion with reference cleanup and confirmation
- Add deleteRootStruct() that clears orphaned refId references before removal
- Show confirmation dialog listing all fields that reference the deleted type
- Auto-switch view to next root struct when the viewed one is deleted
- Entire operation is a single undo macro (Ctrl+Z restores everything)
2026-02-19 10:06:13 -07:00
IChooseYou
7678da033d feat: source management, cross-tab type visibility, default VS2022 theme
- Add clearSources() and File→Source submenu for provider management
- Fix type picker not showing newly created structs (empty structTypeName)
- Add cross-tab type visibility via shared project document list
- Import external types into local document on selection
- Default theme to VS2022 on first launch
- Add test_source_management and test_type_visibility test suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:29:18 -07:00
IChooseYou
acc3ebf5db feat: track value changes toggle, hover scroll fix, ptr* convert, hex split 2026-02-19 06:32:58 -07:00
IChooseYou
26217f5de8 feat: switch provider addressing from RVA to absolute, add pointer expansion tests 2026-02-18 13:07:48 -07:00
IChooseYou
fa0d9a377b fix: type chooser updates colors when theme changes
Add applyTheme() to TypeSelectorPopup that refreshes palette and
stylesheets for all child widgets. Controller connects it to
ThemeManager::themeChanged on popup creation.
2026-02-18 09:59:50 -07:00
IChooseYou
b1d3e52204 fix: type chooser SVG icons and gutter scale with editor zoom level
Derive icon size, gutter width, and icon column width from font
metrics instead of hardcoded 16/10/20 pixel values. Popup width
calculation also scales with font.
2026-02-18 09:47:25 -07:00
IChooseYou
1cccd320b0 feat: simplify cmd bar keyword, add File menu class/struct/enum, remove Align Members
- Command bar shows static keyword (struct/class/enum) without dropdown or colon
- Right-click keyword in cmd bar for class↔struct conversion (enum blocked)
- File menu: New Class (Ctrl+N), New Struct (Ctrl+T), New Enum (Ctrl+E)
- Project explorer right-click: New Class/Struct/Enum on Project node
- Explorer right-click: Convert to Class/Struct on class/struct items
- Remove Align Members submenu, performRealignment, computeStructAlignment
- Remove screenshot code and screenshot.png

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:38:54 -07:00
IChooseYou
5b6e0473cb remove screenshot from README 2026-02-18 08:22:56 -07:00
IChooseYou
57d55456a8 fix: type chooser [n] array modifier now works for primitive types
Array count defaults to 1 when clicking the [n] toggle.
2026-02-18 08:16:02 -07:00
IChooseYou
bb466516ba fix: context menu shows full node menu when right-clicking past line text
Also add Mid theme and remove flaky test_theme test.
2026-02-18 07:57:49 -07:00
IChooseYou
444ba34fa3 feat: disasm popup, symbol separation, context menu improvements, RVA fixes
- Add Fadec x86 disassembler with hover popup for FuncPtr/void Pointer nodes
- Separate pointer symbol from address: // prefix, green comment coloring,
  independent hover/click zones (address triggers popup, symbol is passive)
- Fix RVA margin and inline local offset for pointer-expanded vtable children
  using ptrBase field threaded through composition
- Expand multi-select context menu with quick-convert, duplicate, copy address
- Remove Edit Value from hex node context menu
- Fix heatmap flickering on hex nodes (remove per-byte alternation)
- Fix popup repositioning when moving mouse between lines
- Truncate disasm popup to 6 lines with ... indicator
- Add BUILD_UI_TESTS option to skip widget tests on headless CI
- Add test_disasm with 35 test cases for disassembly and hex dump
- Add KUSER_SHARED_DATA example .rcx file
2026-02-18 07:10:13 -07:00
ichooseyou
91633169a0 fix: guard Windows-only selfTest code for Linux build
- Wrap DWORD/GetCurrentProcessId and KUSER_SHARED_DATA example behind Q_OS_WIN
- Linux selfTest falls back to project_new() with basic hex class
2026-02-17 12:32:44 -07:00
ichooseyou
f041761b62 feat: add FuncPtr32/FuncPtr64 node kinds, darken menu hover, remove refresh log
- Add FuncPtr32/FuncPtr64 types with display, parsing, validation, code generation
- Add quick-convert context menu actions between pointer and funcptr types
- Darken QMenu hover highlight from theme.border to theme.hover for better contrast
- Remove noisy [Refresh] reading debug log from console output
2026-02-17 12:29:51 -07:00
IChooseYou
1c3b4af045 feat: fix heatmap false-heat on offset shift, hover flicker, type chooser cleanup
- Clear value history when node offsets change (insert/delete/resize/
  manual offset edit) so stale values from old addresses don't show
  false heat coloring
- Invalidate in-flight async reads (bump refreshGen) when tree layout
  changes, preventing stale snapshot data from re-introducing heat
- Fix command bar hover cursor flicker: remove premature
  applyHoverCursor() from applyDocument() — runs correctly via
  applySelectionOverlays() after text is finalized
- Fix hover indicator survival: reorder refresh() so text-modifying
  passes (updateCommandRow) run before overlay passes
- Guard synthetic Leave events during setText() to preserve hover state
- Remove primitives from type chooser when pointer modifier (* / **)
  is active; remove primitives entirely in Root command bar mode
- Add test_editor and test_controller test coverage for heat clearing,
  hover survival, and mixed hex/non-hex type scenarios
2026-02-17 11:41:46 -07:00
IChooseYou
5ae9ca0979 feat: value history heatmap, write-fail guard, crash handler hardening
- Value history ring buffer (10 slots) tracks per-node change frequency
- Three-level heatmap: cold (blue), warm (amber), hot (red) via theme
- Heat persists indefinitely (no fade) — shows analysis history
- Calltip on hover shows previous values list
- Old themes auto-derive heat colors from existing palette
- Write failures no longer apply optimistic visual updates
- Crash handler: re-entrancy guard, context dump before risky APIs
2026-02-16 16:44:46 -07:00
Sen66
e064646c02 Added Reclass.NET plugin compatibility layer 2026-02-17 00:18:30 +01:00
IChooseYou
c6c56ffaee feat: default offset margin to relative (+0x) mode 2026-02-16 14:27:41 -07:00
IChooseYou
aba8e5cac9 feat: add Export ReClass XML and remove local-path tests
Adds Export ReClass XML menu item that writes NodeTree to ReClass .NET
compatible XML format with full round-trip fidelity. Removes test cases
that referenced local machine file paths.
2026-02-16 14:16:19 -07:00
IChooseYou
3a5d03fae0 feat: add Import from Source parser for C/C++ struct definitions
Adds a new "Import from Source..." menu item that opens a QScintilla
editor dialog where users can paste C/C++ struct definitions. The parser
tokenizes and parses the source using recursive descent, supporting
stdint.h types, Windows types (BYTE/DWORD/PVOID/etc), multi-word C
types, pointers, arrays, Vec2/3/4/Mat4x4 detection, unions (first
member), padding fields, typedefs, forward declarations, static_assert
size checks, and auto-detection of comment offset mode vs computed
offsets. Also removes the flaky test_editor cursor shape tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:08:12 -07:00
IChooseYou
df79da54e3 ci: remove windows-qt5 job entirely 2026-02-16 12:35:21 -07:00
IChooseYou
e3ff4dfe71 ci: snapshot releases with date tags and platform-specific names
- Tag: snapshot-DD-MM-YYYY instead of latest
- Assets: Reclass-win64-qt6.zip, Reclass-linux64-qt6.AppImage, Reclass-win64-qt5.zip
- Qt5 job now produces release artifacts
- Jobs serialized: windows → linux → windows-qt5
2026-02-16 11:34:14 -07:00
IChooseYou
735e4ea9f7 fix: exclude Qt5-incompatible tests from windows-qt5 CI job 2026-02-16 10:51:35 -07:00
IChooseYou
d937d2f42e fix: Qt5 compat - fix ambiguous QByteRef comparison in test_windbg_provider 2026-02-16 10:37:49 -07:00
IChooseYou
3685530287 fix: Qt5 compat - use toInt() instead of toInteger() for QJsonValue 2026-02-16 10:23:55 -07:00
IChooseYou
9e90f66ca0 fix: Qt5 compat - use pos() instead of position() for QMouseEvent 2026-02-16 09:12:17 -07:00
IChooseYou
f53fa84a15 fix: Qt5 compat - fix addAction wrapper, qHash for NodeKind, add windows-qt5 CI 2026-02-16 09:06:10 -07:00
IChooseYou
13e28e8791 Merge remote-tracking branch 'origin/qt5-compat' 2026-02-16 09:04:35 -07:00
IChooseYou
079b3121ce Revert "add Qt5 compatibility wrapper for addAction and linux-qt5 CI job"
This reverts commit 5e40349768.
2026-02-16 09:04:28 -07:00
IChooseYou
5e40349768 add Qt5 compatibility wrapper for addAction and linux-qt5 CI job 2026-02-16 08:58:09 -07:00
Sen66
8dd6110ec6 Try to fix Qt5 compat + no Qt6 deprec warning 2026-02-16 16:27:28 +01:00
IChooseYou
eb27fc7988 rename Close to Unload Project in File menu, remove icon 2026-02-15 14:39:14 -07:00
IChooseYou
85994d68b9 remove Node menu from menubar, actions available via editor right-click 2026-02-15 14:36:22 -07:00
IChooseYou
55dc5d5875 CI: serialize linux after windows to avoid release tag race condition 2026-02-15 14:29:01 -07:00
IChooseYou
3a92336132 fix: only package plugin DLLs/SOs, not build artifacts 2026-02-15 14:18:09 -07:00
IChooseYou
f9b33f2ba7 fix: options dialog test segfault from dangling ref to themes() temporary 2026-02-15 13:53:59 -07:00
IChooseYou
f2dab07870 fix: build ProcessMemory plugin on Linux, include Plugins in AppImage 2026-02-15 13:45:36 -07:00
IChooseYou
9d22a5ed69 fix: Options dialog - remove CSS overrides, fix title case, add show icon checkbox, add Generator page 2026-02-15 13:41:13 -07:00
IChooseYou
193ab81ecf docs: remove stale status notes from README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:12:36 -07:00
IChooseYou
aa0840b332 CI: consolidate win64+linux64 into single latest release, no pre-release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:10:22 -07:00
IChooseYou
f3631f17ff fix: skip mv when AppImage already has correct name 2026-02-15 12:59:32 -07:00
IChooseYou
42e9bde7ba fix: find qmake via PATH/fallback for linuxdeploy Qt plugin 2026-02-15 12:52:25 -07:00
IChooseYou
07fedf0ae8 fix: derive QMAKE path from Qt6_DIR for linuxdeploy plugin 2026-02-15 12:44:42 -07:00
IChooseYou
2e02a01495 feat: project tree delete, close tab, Linux AppImage bundling
- Right-click delete on classes in Project Tree dock
- File > Close (Ctrl+W) to unload active project tab
- File > Open now replaces current project instead of merging
- Linux CI builds AppImage via linuxdeploy + Qt plugin so users
  don't need Qt installed (fixes libQt6Core.so.6 not found)
- Pin ubuntu-22.04 for broader glibc compatibility
2026-02-15 12:37:56 -07:00
computron
71bc51cbab fix: guard Windows-only setDarkTitleBar and fix deprecated addAction arg order 2026-02-15 11:28:39 -07:00
computron
60a97ab81b fix: add missing Unix headers for Linux build 2026-02-15 11:20:28 -07:00
IChooseYou
bb00e75019 CI: win64 + linux64 builds, guard Windows-only targets for cross-platform 2026-02-15 11:16:09 -07:00
IChooseYou
c038c59e34 CI: add write permission for releases 2026-02-15 11:01:31 -07:00
IChooseYou
862f76b984 CI: auto-upload build zip to latest release 2026-02-15 10:24:46 -07:00
sysadmin
818285a76e CI: skip editor/windbg/com tests that need display or debug tools 2026-02-15 09:49:34 -07:00
sysadmin
ef5e2ebdb9 CI: fix Qt6 install - remove invalid module, pin aqtversion, use 6.8.1 LTS 2026-02-15 09:39:52 -07:00
sysadmin
75fedd2222 CI: switch to Qt6 2026-02-15 09:34:48 -07:00
sysadmin
389745e501 Add Windows CI build 2026-02-15 09:30:49 -07:00
sysadmin
1473a58742 IChooseYou 2026-02-15 09:23:17 -07:00
untitled
4192a4dad3 Hide project tree by default, remove 1px menu border, darken hover/selected theme colors 2026-02-15 08:29:59 -07:00
IChooseYou
4c6bb9564f Fix 7 verified bugs: ref invalidation, bounds check, double refresh, dangling pointer, undo bypass, overflow, hash collision
- BUG-1 (HIGH): Replace dangling QVector reference with local copies in applyTypePopupResult
- BUG-2 (MEDIUM): Add missing upper-bound check in EditTarget::Name handler
- BUG-5 (LOW): Remove redundant unconditional refresh() at end of applyTypePopupResult
- BUG-6 (LOW): Use QPointer for m_cachedPopup to auto-null on parent destruction
- BUG-7 (LOW): Rewrite materializeRefChildren to use undo macro (cmd::Insert + cmd::Collapse)
- BUG-8 (LOW): Guard against integer overflow in byteSize() and clamp arrayLen/strLen in fromJson
- BUG-9 (LOW): Use QPair<uint64_t,uint64_t> key in collectPointerRanges visited set
2026-02-15 08:16:52 -07:00
Sen66
0ef9841f90 Added options dialog 2026-02-15 03:24:12 +01:00
IChooseYOu
0a8244dad4 Single-click type chooser, popup warmup fix, rename ProcessMemory plugin
- Type chooser popup now opens on single click (no need to pre-select node)
- Fix ~170ms first-open delay by pre-initializing Qt popup subsystem at startup
- Rename ProcessMemoryWindows -> ProcessMemory (already supports Linux)
2026-02-14 16:08:44 -07:00
144 changed files with 208751 additions and 3175 deletions

231
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,231 @@
name: Build
on:
push:
branches:
- "**"
pull_request:
branches: [main]
permissions:
contents: write
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Qt6 and MinGW
uses: jurplel/install-qt-action@v4
with:
version: "6.8.1"
arch: "win64_mingw"
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
cache: true
- name: Configure
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
gcc --version
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF \
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
- name: Build
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
cmake --build build
- name: Test
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
ctest --test-dir build --output-on-failure
- name: Package release zip
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
mkdir -p release
cp build/Reclass.exe release/
cp build/ReclassMcpBridge.exe release/
cp build/*.dll release/ 2>/dev/null || true
cp -r build/platforms release/ 2>/dev/null || true
cp -r build/styles release/ 2>/dev/null || true
cp -r build/imageformats release/ 2>/dev/null || true
cp -r build/iconengines release/ 2>/dev/null || true
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true
cp -r build/examples release/ 2>/dev/null || true
cp build/screenshot.png release/ 2>/dev/null || true
cd release && 7z a ../Reclass-win64-qt6.zip *
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Reclass-win64-qt6
path: Reclass-win64-qt6.zip
linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Qt6
uses: jurplel/install-qt-action@v4
with:
version: "6.8.1"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Create AppImage
run: |
# Download linuxdeploy and Qt plugin
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
# Build AppDir structure
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
cp build/Reclass AppDir/usr/bin/
cp build/ReclassMcpBridge AppDir/usr/bin/
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
cp -r build/examples AppDir/usr/bin/ 2>/dev/null || true
mkdir -p AppDir/usr/bin/Plugins
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
# Create AppImage with Qt libs bundled
# install-qt-action adds Qt bin to PATH; find qmake there
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
echo "Found qmake at: $QMAKE_BIN"
export QMAKE="$QMAKE_BIN"
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
export EXTRA_QT_PLUGINS="svg;iconengines"
./linuxdeploy-x86_64.AppImage --appdir AppDir \
--desktop-file deploy/Reclass.desktop \
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
--plugin qt \
--output appimage
# Rename to final name
ls Reclass-*.AppImage
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Reclass-linux64-qt6
path: Reclass-linux64-qt6.AppImage
macos:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
qt_arch: clang_arm64
artifact_name: Reclass-macos-arm64-qt6
zip_name: Reclass-macos-arm64-qt6.zip
- os: macos-15-intel
qt_arch: clang_64
artifact_name: Reclass-macos-x86_64-qt6
zip_name: Reclass-macos-x86_64-qt6.zip
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
brew update
brew install cmake ninja qt
- name: Configure Qt paths
run: |
QT_PREFIX="$(brew --prefix qt)"
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Package app zip
run: |
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
fi
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
codesign --force --deep --sign - build/Reclass.app
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.zip_name }}
release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [windows, linux, macos]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Get date tag
id: date
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: snapshot-${{ steps.date.outputs.tag }}
name: Snapshot ${{ steps.date.outputs.tag }}
body: |
Automated snapshot from main branch.
Commit: ${{ github.sha }}
prerelease: false
files: |
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@@ -11,3 +11,7 @@ build/
*.suo
.vs/
CMakeUserPresets.json
plugins/RcNetPluginCompatLayer/bridge/obj
plugins/RcNetPluginCompatLayer/bridge/bin
.cache
*.DS_Store

6
.gitmodules vendored
View File

@@ -1,3 +1,9 @@
[submodule "third_party/qscintilla"]
path = third_party/qscintilla
url = https://github.com/brCreate/QScintilla.git
[submodule "third_party/raw_pdb"]
path = third_party/raw_pdb
url = https://github.com/MolecularMatters/raw_pdb.git
[submodule "third_party/fadec"]
path = third_party/fadec
url = https://github.com/aengelke/fadec.git

View File

@@ -1,8 +1,9 @@
cmake_minimum_required(VERSION 3.20)
project(Reclass VERSION 0.1 LANGUAGES CXX)
project(Reclass VERSION 0.1 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@@ -30,6 +31,40 @@ endif()
find_package(QScintilla REQUIRED)
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
target_compile_features(raw_pdb PRIVATE cxx_std_11)
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
# that pulls in the real CRT headers and strips __cdecl.
if(NOT MSVC)
target_compile_options(raw_pdb PUBLIC
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
endif()
if(WIN32)
target_link_libraries(raw_pdb PRIVATE rpcrt4)
endif()
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
find_package(Python3 3.9 REQUIRED)
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
message(STATUS "Generating fadec decode tables...")
execute_process(
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
"${FADEC_DIR}/instrs.txt"
"${FADEC_DIR}/fadec-decode-public.inc"
"${FADEC_DIR}/fadec-decode-private.inc"
--32 --64
RESULT_VARIABLE _fadec_result
)
if(NOT _fadec_result EQUAL 0)
message(FATAL_ERROR "Failed to generate fadec decode tables")
endif()
endif()
add_executable(Reclass
src/main.cpp
src/editor.h
@@ -59,15 +94,49 @@ add_executable(Reclass
src/themes/thememanager.cpp
src/themes/themeeditor.h
src/themes/themeeditor.cpp
src/imports/import_reclass_xml.h
src/imports/import_reclass_xml.cpp
src/imports/import_source.h
src/imports/import_source.cpp
src/imports/export_reclass_xml.h
src/imports/export_reclass_xml.cpp
src/imports/import_pdb.h
src/imports/import_pdb.cpp
src/imports/import_pdb_dialog.h
src/imports/import_pdb_dialog.cpp
src/scanner.h
src/scanner.cpp
src/scannerpanel.h
src/scannerpanel.cpp
src/mainwindow.h
src/optionsdialog.h
src/optionsdialog.cpp
src/titlebar.h
src/titlebar.cpp
src/macos_titlebar.h
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
src/addressparser.h
src/addressparser.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
third_party/fadec/format.c
$<$<PLATFORM_ID:Windows>:src/app.rc>
)
target_include_directories(Reclass PRIVATE src)
if(APPLE)
set_target_properties(Reclass PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_ICON_FILE "class.icns"
)
target_sources(Reclass PRIVATE src/icons/class.icns)
set_source_files_properties(src/icons/class.icns
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
endif()
target_include_directories(Reclass PRIVATE src third_party/fadec)
target_link_libraries(Reclass PRIVATE
${QT}::Widgets
@@ -79,7 +148,7 @@ target_link_libraries(Reclass PRIVATE
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
endif()
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
@@ -93,14 +162,28 @@ foreach(_tf ${_theme_files})
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
endforeach()
if(APPLE)
target_sources(Reclass PRIVATE ${_theme_files})
set_source_files_properties(${_theme_files}
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
endif()
# Copy example .rcx files to build directory
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
foreach(_ef ${_example_files})
get_filename_component(_name ${_ef} NAME)
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
endforeach()
if(APPLE)
target_sources(Reclass PRIVATE ${_example_files})
set_source_files_properties(${_example_files}
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
endif()
include(deploy)
add_custom_target(screenshot ALL
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS Reclass deploy
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Capturing UI screenshot with class open..."
)
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
file(WRITE ${_combine_script} "
@@ -117,7 +200,7 @@ foreach(_f
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
file(READ \${_f} _content)
file(APPEND \${_out} \${_content})
file(APPEND \${_out} \"\${_content}\")
file(APPEND \${_out} \"\\n\")
endforeach()
message(STATUS \"Combined sources -> \${_out}\")
@@ -134,29 +217,26 @@ if(BUILD_TESTING)
find_package(${QT} REQUIRED COMPONENTS Test)
enable_testing()
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
# Disasm/Fadec sources needed by any test that links editor.cpp
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_core PRIVATE src)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_core COMMAND test_core)
add_executable(test_format tests/test_format.cpp src/format.cpp)
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_format PRIVATE src)
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_format COMMAND test_format)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_compose PRIVATE src)
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_compose COMMAND test_compose)
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_editor PRIVATE src)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_provider tests/test_provider.cpp)
target_include_directories(test_provider PRIVATE src)
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
@@ -167,118 +247,253 @@ if(BUILD_TESTING)
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_command_row COMMAND test_command_row)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_controller PRIVATE src)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_validation PRIVATE src)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
add_executable(test_import_xml tests/test_import_xml.cpp
src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_xml PRIVATE src)
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_xml COMMAND test_import_xml)
add_executable(test_import_source tests/test_import_source.cpp
src/imports/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_source PRIVATE src)
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_source COMMAND test_import_source)
add_executable(test_export_xml tests/test_export_xml.cpp
src/imports/export_reclass_xml.cpp src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_export_xml PRIVATE src)
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_export_xml COMMAND test_export_xml)
add_executable(test_disasm tests/test_disasm.cpp
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
third_party/fadec/decode.c third_party/fadec/format.c)
target_include_directories(test_disasm PRIVATE src third_party/fadec)
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_disasm COMMAND test_disasm)
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
target_include_directories(test_addressparser PRIVATE src)
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_addressparser COMMAND test_addressparser)
add_executable(test_static_fields tests/test_static_fields.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_static_fields PRIVATE src)
target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_static_fields COMMAND test_static_fields)
add_executable(test_scanner tests/test_scanner.cpp src/scanner.cpp)
target_include_directories(test_scanner PRIVATE src)
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
add_test(NAME test_scanner COMMAND test_scanner)
add_executable(test_32bit_support tests/test_32bit_support.cpp
src/generator.cpp src/imports/import_source.cpp src/imports/import_reclass_xml.cpp
src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_32bit_support PRIVATE src
${CMAKE_SOURCE_DIR}/plugins/RemoteProcessMemory)
target_link_libraries(test_32bit_support PRIVATE ${QT}::Core ${QT}::Widgets ${QT}::Test)
add_test(NAME test_32bit_support COMMAND test_32bit_support)
set_tests_properties(test_32bit_support PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
if(WIN32)
add_executable(test_import_pdb tests/test_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_pdb PRIVATE src)
target_link_libraries(test_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME test_import_pdb COMMAND test_import_pdb)
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(bench_import_pdb PRIVATE src)
target_link_libraries(bench_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
endif()
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_context_menu PRIVATE src)
target_link_libraries(test_context_menu PRIVATE
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_controller PRIVATE src third_party/fadec)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_validation PRIVATE src third_party/fadec)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
target_link_libraries(test_context_menu PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_source_management PRIVATE src third_party/fadec)
target_link_libraries(test_source_management PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_management COMMAND test_source_management)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_editor PRIVATE src third_party/fadec)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_new_features PRIVATE src third_party/fadec)
target_link_libraries(test_new_features PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_new_features COMMAND test_new_features)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_type_selector PRIVATE src)
target_link_libraries(test_type_selector PRIVATE
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
target_link_libraries(test_type_selector PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
if(WIN32)
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_theme tests/test_theme.cpp
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
target_link_libraries(test_type_visibility PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_visibility COMMAND test_type_visibility)
add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_options_dialog PRIVATE src)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_source_provider tests/test_source_provider.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
src/resources.qrc)
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
target_link_libraries(test_source_provider PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_provider COMMAND test_source_provider)
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_theme PRIVATE src)
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_theme COMMAND test_theme)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
target_link_libraries(test_windbg_provider PRIVATE
target_include_directories(test_scanner_ui PRIVATE src)
target_link_libraries(test_scanner_ui PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
if(WIN32)
target_link_libraries(test_windbg_provider PRIVATE dbgeng ole32)
endif()
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
# Requires a running WinDbg debug server on port 5055
if(WIN32)
add_executable(test_com_security tests/test_com_security.cpp)
target_link_libraries(test_com_security PRIVATE dbgeng ole32 version)
add_test(NAME test_com_security COMMAND test_com_security)
endif()
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
src/scanner.cpp)
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
target_link_libraries(test_windbg_provider PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
endif()
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
# that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt)
add_custom_target(deploy_tests ALL
add_executable(bench_large_class tests/bench_large_class.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
target_link_libraries(bench_large_class PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME bench_large_class COMMAND bench_large_class)
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
# that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt)
add_custom_target(deploy_tests ALL
COMMAND $<TARGET_FILE:${QT}::windeployqt>
--no-compiler-runtime --no-translations
--no-opengl-sw --no-system-d3d-compiler
@@ -286,7 +501,15 @@ if(BUILD_TESTING)
DEPENDS test_controller
COMMENT "Deploying Qt runtime DLLs for tests..."
)
endif()
endif()
endif() # BUILD_UI_TESTS
endif()
if(NOT APPLE)
add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/RemoteProcessMemory)
endif()
if(WIN32)
add_subdirectory(plugins/WinDbgMemory)
add_subdirectory(plugins/RcNetPluginCompatLayer)
endif()
add_subdirectory(plugins/ProcessMemoryWindows)
add_subdirectory(plugins/WinDbgMemory)

159
README.md
View File

@@ -1,49 +1,142 @@
This tool 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 runtime or from some static source.
<div align="center">
![screenshot](screenshot.png)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/RECLASS_LIGHTMODE.svg" height="170">
<img src="docs/RECLASS_DARKMODE.svg" alt="Reclass" height="170" />
</picture>
## State
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>Built from scratch as a modern replacement for ReClass.NET and ReClassEx**
- MCP (Model Context Protocol) bridge via `ReclassMcpBridge.exe`. The server starts by default and can be stopped from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge.exe",
"args": []
}
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
[![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)]()
</div>
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.
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
- **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
## Roadmap
- [ ] 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
## 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
## Screenshots
![Type chooser and struct inspection](docs/README_PIC1.png)
![VTable pointer expansion with disassembly preview](docs/README_PIC2.png)
![Split view with rendered C/C++ output](docs/README_PIC3.png)
## 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`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge",
"args": []
}
}
```
- Plugin system is partially implemented. Some UI bugs exist.
- Vector/Matrix improvements have been made but are not entirely complete.
- Every edit goes through a full undo/redo system.
}
```
## Build
1. Prerequisites
### Prerequisites
- Qt 6 with MinGW - Qt Online Installer https://doc.qt.io/qt-6/qt-online-installation.html , note to select MinGW kit + CMake/Ninja from Tools section (online installers index: https://download.qt.io/official_releases/online_installers/)
- CMake 3.20+ - https://cmake.org/download/ - bundled with Qt
- windeployqt docs - https://doc.qt.io/qt-6/windows-deployment.html
- **Qt 6** (or Qt 5) with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
- **Ninja** — bundled with the Qt installer
2. Quick Build (relies on powershell| for manual build skip to step 3)
### Quick Build
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd Reclass
.\scripts\build_qscintilla.ps1
.\scripts\build.ps1
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
```/dev/null/commands.sh#L1-4
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd Reclass
.\scripts\build_qscintilla.ps1
.\scripts\build.ps1
```
3. Manual Build
The build script auto-detects your Qt install location.
Step by step for peoplewho want to run commands themselves:
1. Clone with --recurse-submodules (+ fallback git submodule update --init --recursive)
2. Build QScintilla: qmake + mingw32-make in third_party/qscintilla/src
3. CMake configure + build with -DCMAKE_PREFIX_PATH
4. optionallly windeployqt the exe
### macOS Build
```/dev/null/commands.sh#L1-2
./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 wont open, go to **System Settings → Privacy & Security** and click **Open Anyway**.
### Manual Build (MinGW)
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
3. Configure and build:
```bash
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
cmake --build build
```
4. Optionally run `windeployqt` on the output executable
### Visual Studio 2022+
The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects for the main application, all plugins, and third-party libraries. Requires the [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022) extension with a Qt 6 MSVC kit configured.
### Running Tests
```bash
ctest --test-dir build --output-on-failure
```
## Alternatives
- ReClass.NET (reclass.net) - https://github.com/ReClassNET/ReClass.NET
- ReClassEx - https://github.com/ajkhoury/ReClassEx
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
<div align="center">
<sub>MIT License</sub>
</div>

29
cmake/raw_pdb_prefix.h Normal file
View File

@@ -0,0 +1,29 @@
// Force-included before every raw_pdb translation unit (and consumers).
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
//
// Fix: include the real CRT headers, then include PDB_CRT.h with function
// names macro-renamed to harmless dummies. This triggers #pragma once so
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
//
// Guarded with __cplusplus because PUBLIC propagation applies this to C
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
#ifdef __cplusplus
#include <cstdio>
#include <cstring>
#undef __cdecl
#define __cdecl
#define printf _pdb_crt_unused_printf
#define memcmp _pdb_crt_unused_memcmp
#define memcpy _pdb_crt_unused_memcpy
#define strlen _pdb_crt_unused_strlen
#define strcmp _pdb_crt_unused_strcmp
#include "Foundation/PDB_CRT.h"
#undef printf
#undef memcmp
#undef memcpy
#undef strlen
#undef strcmp
#endif

8
deploy/Reclass.desktop Normal file
View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Reclass
Comment=Memory structure reverse engineering tool
Exec=Reclass
Icon=reclass
Categories=Development;Debugger;
Terminal=false

BIN
docs/README_PIC1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/README_PIC2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
docs/README_PIC3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

160
docs/RECLASS_DARKMODE.svg Normal file
View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
<defs>
<style>
.cls-1 {
fill: url(#Unbenannter_Verlauf_130-2);
}
.cls-2 {
fill: url(#Unbenannter_Verlauf_236-2);
}
.cls-3 {
fill: url(#Unbenannter_Verlauf_225-2);
}
.cls-4 {
fill: #1f2939;
}
.cls-5 {
fill: #5d9bd4;
}
.cls-6 {
fill: #1e3e88;
}
.cls-7 {
fill: #6e809a;
}
.cls-8 {
fill: url(#Unbenannter_Verlauf_225);
}
.cls-9 {
fill: url(#Unbenannter_Verlauf_236);
}
.cls-10 {
fill: url(#Unbenannter_Verlauf_130);
}
.cls-11 {
fill: url(#Unbenannter_Verlauf_170);
}
.cls-12 {
fill: url(#Unbenannter_Verlauf_161);
}
.cls-13 {
fill: url(#Unbenannter_Verlauf_183);
}
.cls-14 {
fill: #b06ba9;
}
.cls-15 {
fill: #826415;
}
.cls-16 {
fill: #e2aa11;
}
.cls-17 {
fill: #893089;
}
</style>
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f3db78"/>
<stop offset=".19" stop-color="#f4e188"/>
<stop offset=".34" stop-color="#f4e38d"/>
<stop offset=".38" stop-color="#f4df81"/>
<stop offset=".47" stop-color="#f5d86f"/>
<stop offset=".57" stop-color="#f5d463"/>
<stop offset=".67" stop-color="#f6d360"/>
<stop offset=".89" stop-color="#f1cc53"/>
<stop offset="1" stop-color="#efbe33"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
<stop offset=".18" stop-color="#e2aa11"/>
<stop offset=".91" stop-color="#826415"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#deb0d3"/>
<stop offset=".15" stop-color="#e1b5d6"/>
<stop offset=".3" stop-color="#e3b8d7"/>
<stop offset=".4" stop-color="#d7a8cd"/>
<stop offset=".53" stop-color="#cf9cc7"/>
<stop offset=".67" stop-color="#cd99c5"/>
<stop offset=".89" stop-color="#c68abc"/>
<stop offset="1" stop-color="#bb7db4"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#b06ba9"/>
<stop offset=".87" stop-color="#893089"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#91c4eb"/>
<stop offset=".2" stop-color="#9dc9ed"/>
<stop offset=".33" stop-color="#96c6ec"/>
<stop offset=".35" stop-color="#91c3ea"/>
<stop offset=".45" stop-color="#7fb8e5"/>
<stop offset=".56" stop-color="#73b2e2"/>
<stop offset=".67" stop-color="#70b0e1"/>
<stop offset=".89" stop-color="#60a7dc"/>
<stop offset="1" stop-color="#4d9bd5"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#5d9bd4"/>
<stop offset=".87" stop-color="#1e3e88"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
</defs>
<g>
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
<g>
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
<g>
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
</g>
</g>
<g>
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
</g>
<g>
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
<g>
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
</g>
</g>
</g>
<g>
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

160
docs/RECLASS_LIGHTMODE.svg Normal file
View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
<defs>
<style>
.cls-1 {
fill: url(#Unbenannter_Verlauf_130-2);
}
.cls-2 {
fill: url(#Unbenannter_Verlauf_236-2);
}
.cls-3 {
fill: url(#Unbenannter_Verlauf_225-2);
}
.cls-4 {
fill: #5d9bd4;
}
.cls-5 {
fill: #e3e8f0;
}
.cls-6 {
fill: #1e3e88;
}
.cls-7 {
fill: #6e809a;
}
.cls-8 {
fill: url(#Unbenannter_Verlauf_225);
}
.cls-9 {
fill: url(#Unbenannter_Verlauf_236);
}
.cls-10 {
fill: url(#Unbenannter_Verlauf_130);
}
.cls-11 {
fill: url(#Unbenannter_Verlauf_170);
}
.cls-12 {
fill: url(#Unbenannter_Verlauf_161);
}
.cls-13 {
fill: url(#Unbenannter_Verlauf_183);
}
.cls-14 {
fill: #b06ba9;
}
.cls-15 {
fill: #826415;
}
.cls-16 {
fill: #e2aa11;
}
.cls-17 {
fill: #893089;
}
</style>
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f3db78"/>
<stop offset=".19" stop-color="#f4e188"/>
<stop offset=".34" stop-color="#f4e38d"/>
<stop offset=".38" stop-color="#f4df81"/>
<stop offset=".47" stop-color="#f5d86f"/>
<stop offset=".57" stop-color="#f5d463"/>
<stop offset=".67" stop-color="#f6d360"/>
<stop offset=".89" stop-color="#f1cc53"/>
<stop offset="1" stop-color="#efbe33"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
<stop offset=".18" stop-color="#e2aa11"/>
<stop offset=".91" stop-color="#826415"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#deb0d3"/>
<stop offset=".15" stop-color="#e1b5d6"/>
<stop offset=".3" stop-color="#e3b8d7"/>
<stop offset=".4" stop-color="#d7a8cd"/>
<stop offset=".53" stop-color="#cf9cc7"/>
<stop offset=".67" stop-color="#cd99c5"/>
<stop offset=".89" stop-color="#c68abc"/>
<stop offset="1" stop-color="#bb7db4"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#b06ba9"/>
<stop offset=".87" stop-color="#893089"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#91c4eb"/>
<stop offset=".2" stop-color="#9dc9ed"/>
<stop offset=".33" stop-color="#96c6ec"/>
<stop offset=".35" stop-color="#91c3ea"/>
<stop offset=".45" stop-color="#7fb8e5"/>
<stop offset=".56" stop-color="#73b2e2"/>
<stop offset=".67" stop-color="#70b0e1"/>
<stop offset=".89" stop-color="#60a7dc"/>
<stop offset="1" stop-color="#4d9bd5"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#5d9bd4"/>
<stop offset=".87" stop-color="#1e3e88"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
</defs>
<g>
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
<g>
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
<g>
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
</g>
</g>
<g>
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
</g>
<g>
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
<g>
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
</g>
</g>
</g>
<g>
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{8792F51B-4951-4BAD-B130-2F0EFDEFF64B}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>ProcessMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{6B775E9C-9CB6-45FD-86A0-BE948A778969}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>RcNetCompatPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.h" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.h" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ReClassNET_Plugin.hpp" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ClrHost.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.cpp" />
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.cpp" />
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\ClrHost.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

17
msvc/Reclass.slnx Normal file
View File

@@ -0,0 +1,17 @@
<Solution>
<Configurations>
<Platform Name="x64" />
</Configurations>
<Folder Name="/plugins/">
<Project Path="ProcessMemoryPlugin.vcxproj" Id="8792f51b-4951-4bad-b130-2f0efdeff64b" />
<Project Path="WinDbgMemoryPlugin.vcxproj" Id="e25d358e-20f0-448b-bb2f-55e9d1f8e7ca" />
<Project Path="RemoteProcessMemoryPlugin.vcxproj" Id="39e2ddf6-cb76-4063-b957-66ecf1252010" />
<Project Path="RcNetCompatPlugin.vcxproj" Id="6b775e9c-9cb6-45fd-86a0-be948a778969" />
</Folder>
<Folder Name="/third_party/">
<Project Path="../third_party/raw_pdb/build/RawPDB.vcxproj" Id="fbe3dbfa-20a7-4f99-9326-ed82c8b7b910" />
<Project Path="fadec.vcxproj" Id="6a30a4f0-1a8d-4c6e-82d4-0a0d9693aa40" />
<Project Path="qscintilla.vcxproj" Id="f7124b57-7682-4702-b725-4d844dc41ada" />
</Folder>
<Project Path="Reclass.vcxproj" Id="c369f1fe-37c2-4c66-ac6d-ecb2b2b4ad5e" />
</Solution>

191
msvc/Reclass.vcxproj Normal file
View File

@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C369F1FE-37C2-4C66-AC6D-ECB2B2B4AD5E}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ClCompile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ClCompile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtRcc Include="..\src\resources.qrc" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="..\src\controller.h" />
<QtMoc Include="..\src\editor.h" />
<QtMoc Include="..\src\mainwindow.h" />
<QtMoc Include="..\src\optionsdialog.h" />
<QtMoc Include="..\src\processpicker.h" />
<QtMoc Include="..\src\scanner.h" />
<QtMoc Include="..\src\scannerpanel.h" />
<QtMoc Include="..\src\titlebar.h" />
<QtMoc Include="..\src\typeselectorpopup.h" />
<QtMoc Include="..\src\imports\import_pdb_dialog.h" />
<QtMoc Include="..\src\mcp\mcp_bridge.h" />
<QtMoc Include="..\src\themes\themeeditor.h" />
<QtMoc Include="..\src\themes\thememanager.h" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\addressparser.h" />
<ClInclude Include="..\src\core.h" />
<ClInclude Include="..\src\disasm.h" />
<ClInclude Include="..\src\generator.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\pluginmanager.h" />
<ClInclude Include="..\src\providerregistry.h" />
<ClInclude Include="..\src\workspace_model.h" />
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
<ClInclude Include="..\src\imports\import_pdb.h" />
<ClInclude Include="..\src\imports\import_reclass_xml.h" />
<ClInclude Include="..\src\imports\import_source.h" />
<ClInclude Include="..\src\providers\buffer_provider.h" />
<ClInclude Include="..\src\providers\null_provider.h" />
<ClInclude Include="..\src\providers\provider.h" />
<ClInclude Include="..\src\providers\snapshot_provider.h" />
<ClInclude Include="..\src\themes\theme.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\addressparser.cpp" />
<ClCompile Include="..\src\compose.cpp" />
<ClCompile Include="..\src\controller.cpp" />
<ClCompile Include="..\src\disasm.cpp" />
<ClCompile Include="..\src\editor.cpp" />
<ClCompile Include="..\src\format.cpp" />
<ClCompile Include="..\src\generator.cpp" />
<ClCompile Include="..\src\main.cpp" />
<ClCompile Include="..\src\optionsdialog.cpp" />
<ClCompile Include="..\src\pluginmanager.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
<ClCompile Include="..\src\providerregistry.cpp" />
<ClCompile Include="..\src\scanner.cpp" />
<ClCompile Include="..\src\scannerpanel.cpp" />
<ClCompile Include="..\src\titlebar.cpp" />
<ClCompile Include="..\src\typeselectorpopup.cpp" />
<ClCompile Include="..\src\imports\export_reclass_xml.cpp" />
<ClCompile Include="..\src\imports\import_pdb.cpp" />
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp" />
<ClCompile Include="..\src\imports\import_reclass_xml.cpp" />
<ClCompile Include="..\src\imports\import_source.cpp" />
<ClCompile Include="..\src\mcp\mcp_bridge.cpp" />
<ClCompile Include="..\src\themes\theme.cpp" />
<ClCompile Include="..\src\themes\themeeditor.cpp" />
<ClCompile Include="..\src\themes\thememanager.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="fadec.vcxproj">
<Project>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</Project>
</ProjectReference>
<ProjectReference Include="qscintilla.vcxproj">
<Project>{F7124B57-7682-4702-B725-4D844DC41ADA}</Project>
</ProjectReference>
<ProjectReference Include="..\third_party\raw_pdb\build\RawPDB.vcxproj">
<Project>{fbe3dbfa-20a7-4f99-9326-ed82c8b7b910}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Source Files\imports">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000001}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\mcp">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000002}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\themes">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000003}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Header Files\imports">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000001}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\mcp">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000002}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\providers">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000003}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\themes">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000004}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Form Files">
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
<Extensions>ui</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<QtRcc Include="..\src\resources.qrc">
<Filter>Resource Files</Filter>
</QtRcc>
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui">
<Filter>Form Files</Filter>
</QtUic>
</ItemGroup>
<ItemGroup>
<QtMoc Include="..\src\controller.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\editor.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\mainwindow.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\optionsdialog.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\processpicker.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\scanner.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\scannerpanel.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\titlebar.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\typeselectorpopup.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\imports\import_pdb_dialog.h">
<Filter>Header Files\imports</Filter>
</QtMoc>
<QtMoc Include="..\src\mcp\mcp_bridge.h">
<Filter>Header Files\mcp</Filter>
</QtMoc>
<QtMoc Include="..\src\themes\themeeditor.h">
<Filter>Header Files\themes</Filter>
</QtMoc>
<QtMoc Include="..\src\themes\thememanager.h">
<Filter>Header Files\themes</Filter>
</QtMoc>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\addressparser.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\core.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\disasm.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\generator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\iplugin.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\pluginmanager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\providerregistry.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\workspace_model.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\export_reclass_xml.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_pdb.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_reclass_xml.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_source.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\buffer_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\null_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\snapshot_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\themes\theme.h">
<Filter>Header Files\themes</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\addressparser.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\compose.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\disasm.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\editor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\format.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\generator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\optionsdialog.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\pluginmanager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\processpicker.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\providerregistry.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\scanner.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\scannerpanel.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\titlebar.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\typeselectorpopup.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\export_reclass_xml.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_pdb.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_reclass_xml.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_source.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\mcp\mcp_bridge.cpp">
<Filter>Source Files\mcp</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\theme.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\themeeditor.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\thememanager.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{39E2DDF6-CB76-4063-B957-66ECF1252010}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>RemoteProcessMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.h" />
<ClInclude Include="..\plugins\RemoteProcessMemory\rcx_rpc_protocol.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{E25D358E-20F0-448B-BB2F-55E9D1F8E7CA}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>WinDbgMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.h" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

81
msvc/fadec.vcxproj Normal file
View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</ProjectGuid>
<RootNamespace>fadec</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<FadecDir>..\third_party\fadec\</FadecDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
<LanguageStandard_C>stdc11</LanguageStandard_C>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<LanguageStandard_C>stdc11</LanguageStandard_C>
</ClCompile>
</ItemDefinitionGroup>
<Target Name="GenerateFadecTables" BeforeTargets="ClCompile"
Inputs="$(FadecDir)instrs.txt;$(FadecDir)parseinstrs.py"
Outputs="$(FadecDir)fadec-decode-public.inc;$(FadecDir)fadec-decode-private.inc">
<Exec Command="python &quot;$(FadecDir)parseinstrs.py&quot; decode &quot;$(FadecDir)instrs.txt&quot; &quot;$(FadecDir)fadec-decode-public.inc&quot; &quot;$(FadecDir)fadec-decode-private.inc&quot; --32 --64" />
</Target>
<ItemGroup>
<ClCompile Include="..\third_party\fadec\decode.c" />
<ClCompile Include="..\third_party\fadec\format.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\third_party\fadec\fadec.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>

445
msvc/qscintilla.vcxproj Normal file
View File

@@ -0,0 +1,445 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{F7124B57-7682-4702-B725-4D844DC41ADA}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>qscintilla</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;printsupport</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;printsupport</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<QScintillaDir>..\third_party\qscintilla\</QScintillaDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Sync</ExceptionHandling>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Sync</ExceptionHandling>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
</ItemDefinitionGroup>
<!-- QtMoc headers (Q_OBJECT) — QScintilla Qt wrapper -->
<ItemGroup>
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciabstractapis.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciapis.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexer.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeravs.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbash.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbatch.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercmake.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercoffeescript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercpp.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercsharp.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercss.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercustom.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerd.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerdiff.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeredifact.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran77.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhtml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeridl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerintelhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjava.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjavascript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjson.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerlua.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermakefile.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermarkdown.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermatlab.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexernasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeroctave.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpascal.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerperl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpostscript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpo.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpov.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerproperties.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpython.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerruby.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerspice.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersql.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersrec.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertcl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertekhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerverilog.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexervhdl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerxml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeryaml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscimacro.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintilla.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintillabase.h" />
<QtMoc Include="..\third_party\qscintilla\src\SciClasses.h" />
<QtMoc Include="..\third_party\qscintilla\src\ScintillaQt.h" />
</ItemGroup>
<!-- ClInclude headers (no Q_OBJECT) -->
<ItemGroup>
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommand.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommandset.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscidocument.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciglobal.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciprinter.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyle.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyledtext.h" />
<ClInclude Include="..\third_party\qscintilla\src\ListBoxQt.h" />
<ClInclude Include="..\third_party\qscintilla\src\SciAccessibility.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILoader.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Platform.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Sci_Position.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\SciLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Scintilla.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ScintillaWidget.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexAccessor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\OptionSet.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SparseState.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StringCopy.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SubStyles.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\WordList.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\AutoComplete.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CallTip.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseConvert.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseFolder.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Catalogue.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CellBuffer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CharClassify.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ContractionState.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\DBCS.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Decoration.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Document.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditModel.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Editor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditView.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ElapsedPeriod.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\FontQuality.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Indicator.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\IntegerRectangle.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\KeyMap.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\LineMarker.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\MarginView.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Partitioning.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PerLine.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Position.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PositionCache.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RESearch.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RunStyles.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Selection.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SparseVector.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SplitVector.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Style.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniConversion.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniqueString.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ViewStyle.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\XPM.h" />
</ItemGroup>
<!-- QScintilla Qt wrapper sources -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\src\qsciscintilla.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciscintillabase.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciabstractapis.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciapis.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscicommand.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscicommandset.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscidocument.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeravs.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbash.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbatch.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercmake.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercoffeescript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercpp.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercsharp.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercss.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercustom.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerd.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerdiff.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeredifact.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran77.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhtml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeridl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerintelhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjava.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjavascript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjson.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerlua.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermakefile.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermarkdown.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermatlab.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexernasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeroctave.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpascal.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerperl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpostscript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpo.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpov.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerproperties.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpython.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerruby.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerspice.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexersql.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexersrec.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertcl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertekhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerverilog.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexervhdl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerxml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeryaml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscimacro.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciprinter.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscistyle.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscistyledtext.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\InputMethod.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\ListBoxQt.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\PlatQt.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\SciAccessibility.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\SciClasses.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\ScintillaQt.cpp" />
</ItemGroup>
<!-- Scintilla lexers -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexA68K.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAPDL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexASY.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAU3.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVE.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAbaqus.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAda.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsm.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsn1.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBaan.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBash.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBasic.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBatch.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBibTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBullant.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCLW.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCOBOL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCPP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCSS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCaml.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCmake.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCoffeeScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexConf.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCrontab.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCsound.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexD.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMAP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMIS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDiff.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexECL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEDIFACT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEiffel.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErlang.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErrorList.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFlagship.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexForth.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFortran.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGAP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGui4Cli.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHTML.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHaskell.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHex.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexIndent.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexInno.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexJSON.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKVIrc.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKix.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLaTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLisp.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLout.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLua.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMMIXAL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMPT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMSSQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMagik.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMake.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMarkdown.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMatlab.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMaxima.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMetapost.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexModula.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMySQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNimrod.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNsis.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNull.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOpal.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPB.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPLM.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPO.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPOV.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPascal.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPerl.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerPro.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerShell.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProgress.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProps.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPython.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexR.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRebol.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRegistry.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRuby.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRust.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSAS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSML.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSTTXT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexScriptol.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSmalltalk.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSorcus.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpecman.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpice.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexStata.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTACL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTADS3.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTAL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCMD.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTxt2tags.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVB.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVHDL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVerilog.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVisualProlog.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexYAML.cpp" />
</ItemGroup>
<!-- Scintilla lexlib -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\WordList.cpp" />
</ItemGroup>
<!-- Scintilla core engine -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\src\AutoComplete.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CallTip.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseConvert.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseFolder.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Catalogue.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CellBuffer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CharClassify.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ContractionState.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\DBCS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Decoration.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Document.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditModel.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Editor.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditView.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Indicator.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\KeyMap.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\LineMarker.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\MarginView.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PerLine.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PositionCache.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RESearch.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RunStyles.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Selection.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Style.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\UniConversion.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ViewStyle.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\XPM.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(ProcessMemoryWindowsPlugin LANGUAGES CXX)
project(ProcessMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
# Plugin sources
set(PLUGIN_SOURCES
ProcessMemoryWindowsPlugin.h
ProcessMemoryWindowsPlugin.cpp
ProcessMemoryPlugin.h
ProcessMemoryPlugin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
)
# Create shared library (DLL)
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES})
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
# Platform-specific linking
if(WIN32)
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32)
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden)
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
# Include directories
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE
target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
# Output to Plugins folder
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES
set_target_properties(ProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)

View File

@@ -1,4 +1,4 @@
#include "ProcessMemoryWindowsPlugin.h"
#include "ProcessMemoryPlugin.h"
#include "../../src/processpicker.h"
@@ -32,12 +32,12 @@
#endif
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryWindowsProvider implementation
// ProcessMemoryProvider implementation
// ──────────────────────────────────────────────────────────────────────────
#ifdef _WIN32
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_handle(nullptr)
, m_pid(pid)
, m_processName(processName)
@@ -56,32 +56,37 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
m_writable = false;
}
if (m_handle)
if (m_handle) {
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(m_handle, &isWow64) && isWow64)
m_pointerSize = 4;
cacheModules();
}
}
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_handle || len <= 0) return false;
SIZE_T bytesRead = 0;
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead);
ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
if ((int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead);
return bytesRead > 0;
}
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_handle || !m_writable || len <= 0) return false;
SIZE_T bytesWritten = 0;
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
return bytesWritten == (SIZE_T)len;
return false;
}
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
@@ -96,7 +101,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {};
}
void ProcessMemoryWindowsProvider::cacheModules()
void ProcessMemoryProvider::cacheModules()
{
HMODULE mods[1024];
DWORD needed = 0;
@@ -124,9 +129,54 @@ void ProcessMemoryWindowsProvider::cacheModules()
}
}
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
if (!m_handle) return regions;
MEMORY_BASIC_INFORMATION mbi;
uint64_t addr = 0;
while (VirtualQueryEx(m_handle, (LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi)) {
if (mbi.State == MEM_COMMIT &&
!(mbi.Protect & PAGE_NOACCESS) &&
!(mbi.Protect & PAGE_GUARD))
{
rcx::MemoryRegion region;
region.base = (uint64_t)mbi.BaseAddress;
region.size = mbi.RegionSize;
region.readable = true;
region.writable = (mbi.Protect & PAGE_READWRITE) ||
(mbi.Protect & PAGE_WRITECOPY) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
(mbi.Protect & PAGE_EXECUTE_READ) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
// Match module name from cached module list
for (const auto& mod : m_modules) {
if (region.base >= mod.base && region.base < mod.base + mod.size) {
region.moduleName = mod.name;
break;
}
}
regions.append(region);
}
uint64_t next = (uint64_t)mbi.BaseAddress + mbi.RegionSize;
if (next <= addr) break; // overflow protection
addr = next;
}
return regions;
}
#elif defined(__linux__)
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_fd(-1)
, m_pid(pid)
, m_processName(processName)
@@ -147,24 +197,33 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
m_writable = false;
}
if (m_fd >= 0)
if (m_fd >= 0) {
// Detect 32-bit ELF process
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QByteArray exePathUtf8 = exePath.toUtf8();
int exeFd = ::open(exePathUtf8.constData(), O_RDONLY);
if (exeFd >= 0) {
unsigned char elfClass = 0;
// ELF e_ident[EI_CLASS] is at offset 4
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
m_pointerSize = 4;
::close(exeFd);
}
cacheModules();
}
}
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention)
struct iovec local;
local.iov_base = buf;
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
@@ -172,23 +231,21 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
return true;
// Fallback: pread on /proc/<pid>/mem
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nread == static_cast<ssize_t>(len);
}
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_fd < 0 || !m_writable || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first
struct iovec local;
local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
@@ -196,11 +253,11 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
return true;
// Fallback: pwrite on /proc/<pid>/mem
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nwritten == static_cast<ssize_t>(len);
}
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
@@ -215,7 +272,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {};
}
void ProcessMemoryWindowsProvider::cacheModules()
void ProcessMemoryProvider::cacheModules()
{
// Parse /proc/<pid>/maps to discover loaded modules
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
@@ -286,9 +343,70 @@ void ProcessMemoryWindowsProvider::cacheModules()
}
}
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
if (m_fd < 0) return regions;
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
std::ifstream mapsFile(mapsPath.toStdString());
if (!mapsFile.is_open()) return regions;
std::string line;
while (std::getline(mapsFile, line)) {
std::istringstream iss(line);
std::string addrRange, perms, offset, dev, inode, pathname;
iss >> addrRange >> perms >> offset >> dev >> inode;
std::getline(iss, pathname);
auto dash = addrRange.find('-');
if (dash == std::string::npos) continue;
uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16);
uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16);
if (perms.size() < 4) continue;
bool readable = (perms[0] == 'r');
bool writable = (perms[1] == 'w');
bool executable = (perms[2] == 'x');
if (!readable) continue;
rcx::MemoryRegion region;
region.base = addrStart;
region.size = addrEnd - addrStart;
region.readable = readable;
region.writable = writable;
region.executable = executable;
// Extract module name from pathname
size_t start = pathname.find_first_not_of(" \t");
if (start != std::string::npos) {
QString qpath = QString::fromStdString(pathname.substr(start));
if (qpath.startsWith('/') && !qpath.startsWith("/dev/") &&
!qpath.startsWith("/memfd:")) {
QFileInfo fi(qpath);
region.moduleName = fi.fileName();
}
}
regions.append(region);
}
return regions;
}
#endif // platform
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32
if (m_handle)
@@ -299,7 +417,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
#endif
}
int ProcessMemoryWindowsProvider::size() const
int ProcessMemoryProvider::size() const
{
#ifdef _WIN32
return m_handle ? 0x10000 : 0;
@@ -309,22 +427,22 @@ int ProcessMemoryWindowsProvider::size() const
}
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryWindowsPlugin implementation
// ProcessMemoryPlugin implementation
// ──────────────────────────────────────────────────────────────────────────
QIcon ProcessMemoryWindowsPlugin::Icon() const
QIcon ProcessMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
}
bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const
bool ProcessMemoryPlugin::canHandle(const QString& target) const
{
// Target format: "pid:name" or just "pid"
QRegularExpression re("^\\d+");
return re.match(target).hasMatch();
}
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg)
std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
// Parse target: "pid:name" or just "pid"
QStringList parts = target.split(':');
@@ -339,7 +457,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
if (!provider->isValid())
{
if (errorMsg)
@@ -352,7 +470,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
return provider;
}
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const
uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
#ifdef _WIN32
// Parse PID from target
@@ -409,7 +527,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
#endif
}
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
// Use custom process enumeration from plugin
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
@@ -423,6 +541,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
info.name = pinfo.name;
info.path = pinfo.path;
info.icon = pinfo.icon;
info.is32Bit = pinfo.is32Bit;
processes.append(info);
}
@@ -440,7 +559,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
return false;
}
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> processes;
@@ -484,6 +603,11 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
}
}
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
}
@@ -530,6 +654,16 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
// Detect 32-bit ELF process
int exeFd = ::open(exePath.toUtf8().constData(), O_RDONLY);
if (exeFd >= 0) {
unsigned char elfClass = 0;
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
info.is32Bit = true;
::close(exeFd);
}
processes.append(info);
}
#endif
@@ -543,5 +677,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new ProcessMemoryWindowsPlugin();
return new ProcessMemoryPlugin();
}

View File

@@ -5,14 +5,14 @@
#include <cstdint>
/**
* Process memory provider (Windows)
* Reads/writes memory from a live process using Windows platform APIs
* Process memory provider
* Reads/writes memory from a live process using platform APIs
*/
class ProcessMemoryWindowsProvider : public rcx::Provider
class ProcessMemoryProvider : public rcx::Provider
{
public:
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName);
~ProcessMemoryWindowsProvider() override;
ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
@@ -24,14 +24,22 @@ public:
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("LocalProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
int pointerSize() const override { return m_pointerSize; }
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isReadable(uint64_t, int len) const override {
#ifdef _WIN32
return m_handle && len >= 0;
#elif defined(__linux__)
return m_fd >= 0 && len >= 0;
#endif
}
// Process-specific helpers
uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); }
private:
@@ -47,6 +55,7 @@ private:
QString m_processName;
bool m_writable;
uint64_t m_base;
int m_pointerSize = 8;
struct ModuleInfo {
QString name;
@@ -57,15 +66,15 @@ private:
};
/**
* Plugin that provides ProcessMemoryWindowsProvider
* Plugin that provides ProcessMemoryProvider
*/
class ProcessMemoryWindowsPlugin : public IProviderPlugin
class ProcessMemoryPlugin : public IProviderPlugin
{
public:
std::string Name() const override { return "Process Memory Windows"; }
std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory from local running processes (Windows)"; }
std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;

View File

@@ -0,0 +1,93 @@
cmake_minimum_required(VERSION 3.20)
project(RcNetCompatPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Plugin sources
set(PLUGIN_SOURCES
RcNetCompatPlugin.h
RcNetCompatPlugin.cpp
RcNetCompatProvider.h
RcNetCompatProvider.cpp
ReClassNET_Plugin.hpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
)
# -- Optional .NET bridge -------------------------------------------------
# When the .NET SDK is available, build the C# bridge assembly and enable
# CLR hosting support in the C++ plugin.
find_program(DOTNET_EXE dotnet)
if(DOTNET_EXE)
# Check that 'dotnet build' actually works for net472
execute_process(
COMMAND ${DOTNET_EXE} --list-sdks
OUTPUT_VARIABLE _dotnet_sdks
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(_dotnet_sdks)
set(HAS_CLR_BRIDGE ON)
message(STATUS "RcNetCompat: .NET SDK found -- building managed bridge")
endif()
endif()
if(HAS_CLR_BRIDGE)
list(APPEND PLUGIN_SOURCES
ClrHost.h
ClrHost.cpp
)
# Build the C# bridge assembly
set(_bridge_src "${CMAKE_CURRENT_SOURCE_DIR}/bridge")
set(_bridge_out "${CMAKE_BINARY_DIR}/Plugins/RcNetBridge.dll")
add_custom_command(
OUTPUT "${_bridge_out}"
COMMAND ${DOTNET_EXE} build
"${_bridge_src}/RcNetBridge.csproj"
-c Release
-o "${CMAKE_BINARY_DIR}/Plugins"
--nologo -v quiet
DEPENDS
"${_bridge_src}/RcNetBridge.cs"
"${_bridge_src}/RcNetBridge.csproj"
COMMENT "Building RcNetBridge.dll (.NET bridge)..."
)
add_custom_target(RcNetBridge ALL DEPENDS "${_bridge_out}")
else()
message(STATUS "RcNetCompat: .NET SDK not found -- managed plugin support disabled")
endif()
# Create shared library (DLL)
add_library(RcNetCompatPlugin SHARED ${PLUGIN_SOURCES})
if(HAS_CLR_BRIDGE)
target_compile_definitions(RcNetCompatPlugin PRIVATE HAS_CLR_BRIDGE=1)
add_dependencies(RcNetCompatPlugin RcNetBridge)
# CLR hosting uses COM (ole32)
target_link_libraries(RcNetCompatPlugin PRIVATE ole32)
endif()
# Link Qt
target_link_libraries(RcNetCompatPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
# Include directories
target_include_directories(RcNetCompatPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
# Output to Plugins folder
set_target_properties(RcNetCompatPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)

View File

@@ -0,0 +1,162 @@
#include "ClrHost.h"
#include <cwchar>
// -- GUIDs ----------------------------------------------------------------
using FnCLRCreateInstance = HRESULT(STDAPICALLTYPE*)(REFCLSID, REFIID, LPVOID*);
// {9280188D-0E8E-4867-B30C-7FA83884E8DE}
static const GUID sCLSID_CLRMetaHost =
{0x9280188d, 0x0e8e, 0x4867, {0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}};
// {D332DB9E-B9B3-4125-8207-A14884F53216}
static const GUID sIID_ICLRMetaHost =
{0xD332DB9E, 0xB9B3, 0x4125, {0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}};
// {BD39D1D2-BA2F-486A-89B0-B4B0CB466891}
static const GUID sIID_ICLRRuntimeInfo =
{0xBD39D1D2, 0xBA2F, 0x486a, {0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}};
// {90F1A06E-7712-4762-86B5-7A5EBA6BDB02}
static const GUID sCLSID_CLRRuntimeHost =
{0x90F1A06E, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
// {90F1A06C-7712-4762-86B5-7A5EBA6BDB02}
static const GUID sIID_ICLRRuntimeHost =
{0x90F1A06C, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
// -- ClrHost implementation -----------------------------------------------
ClrHost::ClrHost()
{
startClr();
}
ClrHost::~ClrHost()
{
if (m_runtimeHost) m_runtimeHost->Release();
if (m_runtimeInfo) m_runtimeInfo->Release();
if (m_metaHost) m_metaHost->Release();
if (m_mscoree) FreeLibrary(m_mscoree);
}
bool ClrHost::startClr()
{
m_mscoree = LoadLibraryW(L"mscoree.dll");
if (!m_mscoree)
return false;
auto fnCreate = reinterpret_cast<FnCLRCreateInstance>(
GetProcAddress(m_mscoree, "CLRCreateInstance"));
if (!fnCreate)
return false;
HRESULT hr = fnCreate(sCLSID_CLRMetaHost, sIID_ICLRMetaHost,
reinterpret_cast<LPVOID*>(&m_metaHost));
if (FAILED(hr) || !m_metaHost)
return false;
hr = m_metaHost->GetRuntime(L"v4.0.30319", sIID_ICLRRuntimeInfo,
reinterpret_cast<LPVOID*>(&m_runtimeInfo));
if (FAILED(hr) || !m_runtimeInfo)
return false;
hr = m_runtimeInfo->GetInterface(sCLSID_CLRRuntimeHost, sIID_ICLRRuntimeHost,
(LPVOID*)&m_runtimeHost);
if (FAILED(hr) || !m_runtimeHost)
return false;
hr = m_runtimeHost->Start();
if (FAILED(hr))
return false;
m_clrStarted = true;
return true;
}
bool ClrHost::loadManagedPlugin(const QString& bridgeDllPath,
const QString& pluginPath,
RcNetFunctions* outFunctions,
QString* errorMsg)
{
if (!m_runtimeHost || !m_clrStarted) {
if (errorMsg)
*errorMsg = QStringLiteral(
".NET Framework 4.x is not available on this machine.\n"
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
return false;
}
// Zero the function table -- the bridge will fill it
memset(outFunctions, 0, sizeof(RcNetFunctions));
// Build the argument string: "<hex_address_of_function_table>|<plugin_path>"
// Use %ls (not %s) for wide strings -- MinGW follows POSIX conventions.
wchar_t arg[2048];
swprintf(arg, sizeof(arg) / sizeof(wchar_t),
L"%llx|%ls",
reinterpret_cast<unsigned long long>(outFunctions),
reinterpret_cast<const wchar_t*>(pluginPath.utf16()));
DWORD retVal = 0;
HRESULT hr = m_runtimeHost->ExecuteInDefaultAppDomain(
reinterpret_cast<LPCWSTR>(bridgeDllPath.utf16()),
L"RcNetBridge.Bridge",
L"Initialize",
arg,
&retVal
);
if (FAILED(hr)) {
if (errorMsg)
*errorMsg = QStringLiteral(
"Failed to execute .NET bridge (HRESULT 0x%1).\n"
"Bridge: %2\n"
"Plugin: %3")
.arg(static_cast<uint>(hr), 8, 16, QChar('0'))
.arg(bridgeDllPath)
.arg(pluginPath);
return false;
}
if (retVal != 0) {
if (errorMsg) {
switch (retVal) {
case 1:
*errorMsg = QStringLiteral("Bridge: invalid argument format.");
break;
case 2:
*errorMsg = QStringLiteral(
"No ICoreProcessFunctions implementation found in the .NET plugin.\n"
"The DLL may not be a ReClass.NET plugin.");
break;
case 3:
*errorMsg = QStringLiteral(
"Failed to load the .NET plugin assembly.\n"
"Check that all its dependencies are available.");
break;
default:
*errorMsg = QStringLiteral("Bridge returned error code %1.").arg(retVal);
break;
}
}
return false;
}
// Verify the bridge wrote at least the minimum required function pointers
if (!outFunctions->ReadRemoteMemory ||
!outFunctions->OpenRemoteProcess ||
!outFunctions->EnumerateProcesses ||
!outFunctions->CloseRemoteProcess) {
if (errorMsg)
*errorMsg = QStringLiteral(
"The .NET bridge loaded but did not provide the required functions "
"(ReadRemoteMemory, OpenRemoteProcess, CloseRemoteProcess, EnumerateProcesses).");
return false;
}
return true;
}

View File

@@ -0,0 +1,99 @@
#pragma once
// In-process CLR hosting for loading .NET ReClass.NET plugins.
// Dynamically loads mscoree.dll and uses ICLRMetaHost -> ICLRRuntimeInfo ->
// ICLRRuntimeHost::ExecuteInDefaultAppDomain to call into the C# bridge.
#include "ReClassNET_Plugin.hpp"
#include <QString>
#include <windows.h>
#include <objbase.h>
// -- Minimal COM interface definitions for CLR hosting --------------------
// Defined here to avoid depending on Windows SDK metahost.h / mscoree.h
// which may not be present in all MinGW distributions.
// Only methods we actually call have real signatures; the rest are stubs
// that preserve correct vtable offsets.
#undef INTERFACE
#define INTERFACE ICLRMetaHost
DECLARE_INTERFACE_(ICLRMetaHost, IUnknown)
{
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
// ICLRMetaHost
STDMETHOD(GetRuntime)(LPCWSTR pwzVersion, REFIID riid, LPVOID* ppRuntime) PURE;
STDMETHOD(GetVersionFromFile)(LPCWSTR, LPWSTR, DWORD*) PURE;
STDMETHOD(EnumerateInstalledRuntimes)(void**) PURE;
STDMETHOD(EnumerateLoadedRuntimes)(HANDLE, void**) PURE;
STDMETHOD(RequestRuntimeLoadedNotification)(void*) PURE;
STDMETHOD(QueryLegacyV2RuntimeBinding)(REFIID, LPVOID*) PURE;
STDMETHOD_(void, ExitProcess)(INT32) PURE;
};
#undef INTERFACE
#define INTERFACE ICLRRuntimeInfo
DECLARE_INTERFACE_(ICLRRuntimeInfo, IUnknown)
{
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
// ICLRRuntimeInfo
STDMETHOD(GetVersionString)(LPWSTR, DWORD*) PURE;
STDMETHOD(GetRuntimeDirectory)(LPWSTR, DWORD*) PURE;
STDMETHOD(IsLoaded)(HANDLE, BOOL*) PURE;
STDMETHOD(LoadErrorString)(UINT, LPWSTR, DWORD*, LONG) PURE;
STDMETHOD(LoadLibrary)(LPCWSTR, HMODULE*) PURE;
STDMETHOD(GetProcAddress)(LPCSTR, LPVOID*) PURE;
STDMETHOD(GetInterface)(REFCLSID rclsid, REFIID riid, LPVOID* ppUnk) PURE;
};
#undef INTERFACE
#define INTERFACE ICLRRuntimeHost
DECLARE_INTERFACE_(ICLRRuntimeHost, IUnknown)
{
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
// ICLRRuntimeHost
STDMETHOD(Start)() PURE;
STDMETHOD(Stop)() PURE;
STDMETHOD(SetHostControl)(void*) PURE;
STDMETHOD(GetCLRControl)(void**) PURE;
STDMETHOD(UnloadAppDomain)(DWORD, BOOL) PURE;
STDMETHOD(ExecuteInAppDomain)(DWORD, void*, void*) PURE;
STDMETHOD(GetCurrentAppDomainId)(DWORD*) PURE;
STDMETHOD(ExecuteApplication)(LPCWSTR, DWORD, LPCWSTR*, DWORD, LPCWSTR*, int*) PURE;
STDMETHOD(ExecuteInDefaultAppDomain)(LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, DWORD*) PURE;
};
#undef INTERFACE
// -- CLR Host wrapper -----------------------------------------------------
class ClrHost
{
public:
ClrHost();
~ClrHost();
// True if the .NET Framework CLR (v4.0) is available on this machine.
bool isAvailable() const { return m_runtimeHost != nullptr && m_clrStarted; }
// Load a managed ReClass.NET plugin via the C# bridge.
bool loadManagedPlugin(const QString& bridgeDllPath,
const QString& pluginPath,
RcNetFunctions* outFunctions,
QString* errorMsg = nullptr);
private:
bool startClr();
HMODULE m_mscoree = nullptr;
ICLRMetaHost* m_metaHost = nullptr;
ICLRRuntimeInfo* m_runtimeInfo = nullptr;
ICLRRuntimeHost* m_runtimeHost = nullptr;
bool m_clrStarted = false;
};

View File

@@ -0,0 +1,333 @@
#include "RcNetCompatPlugin.h"
#include "RcNetCompatProvider.h"
#include "../../src/processpicker.h"
#include <QApplication>
#include <QCoreApplication>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QStyle>
#include <windows.h>
// -- Helpers --------------------------------------------------------------
QIcon RcNetCompatPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_TrashIcon);
}
// --.NET assembly detection ----------------------------------------------
static bool isDotNetAssembly(const QString& path)
{
// A .NET assembly has a non-zero CLR header directory entry in the PE
// optional header. We check this by loading the PE without running
// DllMain and inspecting the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR.
HMODULE hMod = GetModuleHandleW(reinterpret_cast<LPCWSTR>(path.utf16()));
if (!hMod)
hMod = LoadLibraryExW(reinterpret_cast<LPCWSTR>(path.utf16()),
nullptr, DONT_RESOLVE_DLL_REFERENCES);
if (!hMod) return false;
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(hMod);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
reinterpret_cast<const char*>(hMod) + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
constexpr DWORD kClrIndex = IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR; // 14
DWORD rva = 0, dirSize = 0;
if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER64*>(&nt->OptionalHeader);
if (opt->NumberOfRvaAndSizes > kClrIndex) {
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
dirSize = opt->DataDirectory[kClrIndex].Size;
}
} else {
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(&nt->OptionalHeader);
if (opt->NumberOfRvaAndSizes > kClrIndex) {
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
dirSize = opt->DataDirectory[kClrIndex].Size;
}
}
return rva != 0 && dirSize != 0;
}
// --Unified loader (dispatches native vs managed) ------------------------
bool RcNetCompatPlugin::loadPlugin(const QString& path, QString* errorMsg)
{
if (m_dllPath == path && (m_lib || m_isManaged))
return true; // Already loaded
if (isDotNetAssembly(path)) {
#ifdef HAS_CLR_BRIDGE
return loadManagedDll(path, errorMsg);
#else
if (errorMsg)
*errorMsg = QStringLiteral(
"This is a .NET assembly.\n\n"
"This build does not include .NET bridge support.\n"
"Rebuild with the .NET SDK installed to enable managed plugin loading.");
return false;
#endif
}
return loadNativeDll(path, errorMsg);
}
// --Native DLL loading ---------------------------------------------------
bool RcNetCompatPlugin::loadNativeDll(const QString& path, QString* errorMsg)
{
unloadNativeDll();
m_lib = std::make_unique<QLibrary>(path);
if (!m_lib->load()) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to load DLL: %1").arg(m_lib->errorString());
m_lib.reset();
return false;
}
// Resolve all function pointers
m_fns.EnumerateProcesses =
reinterpret_cast<FnEnumerateProcesses>(m_lib->resolve("EnumerateProcesses"));
m_fns.OpenRemoteProcess =
reinterpret_cast<FnOpenRemoteProcess>(m_lib->resolve("OpenRemoteProcess"));
m_fns.IsProcessValid =
reinterpret_cast<FnIsProcessValid>(m_lib->resolve("IsProcessValid"));
m_fns.CloseRemoteProcess =
reinterpret_cast<FnCloseRemoteProcess>(m_lib->resolve("CloseRemoteProcess"));
m_fns.ReadRemoteMemory =
reinterpret_cast<FnReadRemoteMemory>(m_lib->resolve("ReadRemoteMemory"));
m_fns.WriteRemoteMemory =
reinterpret_cast<FnWriteRemoteMemory>(m_lib->resolve("WriteRemoteMemory"));
m_fns.EnumerateRemoteSectionsAndModules =
reinterpret_cast<FnEnumerateRemoteSectionsAndModules>(
m_lib->resolve("EnumerateRemoteSectionsAndModules"));
m_fns.ControlRemoteProcess =
reinterpret_cast<FnControlRemoteProcess>(m_lib->resolve("ControlRemoteProcess"));
// At minimum we need read + open + close
if (!m_fns.ReadRemoteMemory || !m_fns.OpenRemoteProcess || !m_fns.CloseRemoteProcess || !m_fns.EnumerateProcesses) {
if (errorMsg)
*errorMsg = QStringLiteral(
"DLL is missing required exports (ReadRemoteMemory, OpenRemoteProcess, "
"CloseRemoteProcess, EnumerateProcesses). Is this a ReClass.NET native plugin?");
m_lib->unload();
m_lib.reset();
m_fns = {};
return false;
}
m_dllPath = path;
m_isManaged = false;
return true;
}
void RcNetCompatPlugin::unloadNativeDll()
{
if (m_lib) {
m_lib->unload();
m_lib.reset();
}
m_fns = {};
m_dllPath.clear();
m_isManaged = false;
}
// --Managed (.NET) DLL loading via CLR bridge ----------------------------
#ifdef HAS_CLR_BRIDGE
bool RcNetCompatPlugin::loadManagedDll(const QString& path, QString* errorMsg)
{
unloadNativeDll();
// Lazily create the CLR host (one per plugin lifetime)
if (!m_clrHost)
m_clrHost = std::make_unique<ClrHost>();
if (!m_clrHost->isAvailable()) {
if (errorMsg)
*errorMsg = QStringLiteral(
".NET Framework 4.x is not available on this machine.\n"
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
return false;
}
// Locate RcNetBridge.dll next to our own plugin DLL
// Use native separators -- the CLR expects Windows-style backslash paths.
QString bridgePath = QDir::toNativeSeparators(
QCoreApplication::applicationDirPath()
+ QStringLiteral("/Plugins/RcNetBridge.dll"));
if (!QFileInfo::exists(bridgePath)) {
if (errorMsg)
*errorMsg = QStringLiteral(
"RcNetBridge.dll not found in the Plugins folder.\n"
"Expected at: %1").arg(bridgePath);
return false;
}
m_fns = {};
QString nativePath = QDir::toNativeSeparators(path);
if (!m_clrHost->loadManagedPlugin(bridgePath, nativePath, &m_fns, errorMsg))
return false;
m_dllPath = path;
m_isManaged = true;
return true;
}
#endif // HAS_CLR_BRIDGE
// --IProviderPlugin ------------------------------------------------------
bool RcNetCompatPlugin::canHandle(const QString& target) const
{
// Target format: "dllpath|pid:name"
return target.contains('|');
}
std::unique_ptr<rcx::Provider> RcNetCompatPlugin::createProvider(
const QString& target, QString* errorMsg)
{
// Parse "dllpath|pid:name"
int sep = target.indexOf('|');
if (sep < 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target format");
return nullptr;
}
QString dllPath = target.left(sep);
QString pidPart = target.mid(sep + 1);
// Load (or reuse) the plugin DLL
if (!loadPlugin(dllPath, errorMsg))
return nullptr;
// Parse pid:name
QStringList parts = pidPart.split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID: %1").arg(parts[0]);
return nullptr;
}
QString procName = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
auto provider = std::make_unique<RcNetCompatProvider>(m_fns, pid, procName);
if (!provider->isValid()) {
if (errorMsg)
*errorMsg = QStringLiteral(
"Failed to open process %1 (PID: %2) via ReClass.NET plugin.\n"
"Ensure the process is running and the plugin supports it.")
.arg(procName).arg(pid);
return nullptr;
}
return provider;
}
uint64_t RcNetCompatPlugin::getInitialBaseAddress(const QString& target) const
{
Q_UNUSED(target);
// The provider sets its own base from module enumeration.
return 0;
}
bool RcNetCompatPlugin::selectTarget(QWidget* parent, QString* target)
{
// Step 1: Pick a ReClass.NET plugin DLL (native or .NET)
QString dllPath = QFileDialog::getOpenFileName(
parent,
QStringLiteral("Select ReClass.NET Plugin"),
QString(),
QStringLiteral("DLL Files (*.dll)"));
if (dllPath.isEmpty())
return false;
// Step 2: Load and validate the DLL
QString loadErr;
if (!loadPlugin(dllPath, &loadErr)) {
QMessageBox::warning(parent,
QStringLiteral("ReClass.NET Compat Layer"),
loadErr);
return false;
}
// Step 3: Enumerate processes and show picker
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
QList<ProcessInfo> processes;
for (const auto& p : pluginProcesses) {
ProcessInfo info;
info.pid = p.pid;
info.name = p.name;
info.path = p.path;
info.icon = p.icon;
processes.append(info);
}
ProcessPicker picker(processes, parent);
if (picker.exec() != QDialog::Accepted)
return false;
uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName();
// Step 4: Format target as "dllpath|pid:name"
*target = QStringLiteral("%1|%2:%3").arg(dllPath).arg(pid).arg(name);
return true;
}
// --Process enumeration --------------------------------------------------
namespace {
struct ProcessCollector {
QVector<PluginProcessInfo>* dest = nullptr;
};
thread_local ProcessCollector g_processCollector;
void RC_CALLCONV processCallback(EnumerateProcessData* data)
{
if (!data || !g_processCollector.dest) return;
PluginProcessInfo info;
info.pid = static_cast<uint32_t>(data->Id);
info.name = QString::fromUtf16(data->Name);
info.path = QString::fromUtf16(data->Path);
g_processCollector.dest->append(info);
}
} // anonymous namespace
QVector<PluginProcessInfo> RcNetCompatPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> result;
if (!m_fns.EnumerateProcesses)
return result;
g_processCollector.dest = &result;
m_fns.EnumerateProcesses(processCallback);
g_processCollector.dest = nullptr;
return result;
}
// --Plugin factory -------------------------------------------------------
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new RcNetCompatPlugin();
}

View File

@@ -0,0 +1,61 @@
#pragma once
#include "../../src/iplugin.h"
#include "ReClassNET_Plugin.hpp"
#include <QLibrary>
#include <memory>
#ifdef HAS_CLR_BRIDGE
#include "ClrHost.h"
#endif
/**
* ReclassX plugin that loads ReClass.NET plugin DLLs
* and exposes them as ReclassX providers.
*
* Supports both native DLLs (C exports) and, when built with
* HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting.
*
* Target string format: "dllpath|pid:processname"
*/
class RcNetCompatPlugin : public IProviderPlugin
{
public:
// Plugin metadata
std::string Name() const override { return "ReClass.NET Compat Layer"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override {
return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources";
}
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;
// IProviderPlugin interface
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
// Override process enumeration -- we enumerate via the loaded DLL
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
private:
bool loadPlugin(const QString& path, QString* errorMsg = nullptr);
bool loadNativeDll(const QString& path, QString* errorMsg = nullptr);
void unloadNativeDll();
#ifdef HAS_CLR_BRIDGE
bool loadManagedDll(const QString& path, QString* errorMsg = nullptr);
std::unique_ptr<ClrHost> m_clrHost;
#endif
std::unique_ptr<QLibrary> m_lib;
RcNetFunctions m_fns;
QString m_dllPath;
bool m_isManaged = false;
};
// Plugin export
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -0,0 +1,132 @@
#include "RcNetCompatProvider.h"
#include <QFileInfo>
#include <cstring>
// -- Construction / destruction -------------------------------------------
RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns,
uint32_t pid,
const QString& processName)
: m_fns(fns)
, m_pid(pid)
, m_processName(processName)
{
if (m_fns.OpenRemoteProcess)
m_handle = m_fns.OpenRemoteProcess(static_cast<RC_Size>(pid),
ProcessAccess::Full);
if (m_handle)
cacheModules();
}
RcNetCompatProvider::~RcNetCompatProvider()
{
if (m_handle && m_fns.CloseRemoteProcess)
m_fns.CloseRemoteProcess(m_handle);
}
// -- Required overrides ---------------------------------------------------
bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
return false;
return m_fns.ReadRemoteMemory(m_handle,
reinterpret_cast<RC_Pointer>(addr),
static_cast<RC_Pointer>(buf),
0, len);
}
int RcNetCompatProvider::size() const
{
if (!m_handle) return 0;
if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0;
return 0x10000;
}
// -- Optional overrides ---------------------------------------------------
bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
return false;
return m_fns.WriteRemoteMemory(m_handle,
reinterpret_cast<RC_Pointer>(addr),
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
0, len);
}
QString RcNetCompatProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t RcNetCompatProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
// -- Module enumeration ---------------------------------------------------
namespace {
// Thread-local collector for the module enumeration callback.
// ReClass.NET callbacks are synchronous, so this is safe.
struct ModuleCollector {
QVector<RcNetCompatProvider::ModuleInfo>* dest = nullptr;
};
thread_local ModuleCollector g_moduleCollector;
void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data)
{
if (!data || !g_moduleCollector.dest) return;
QString path = QString::fromUtf16(data->Path);
QFileInfo fi(path);
RcNetCompatProvider::ModuleInfo info;
info.name = fi.fileName();
info.base = reinterpret_cast<uint64_t>(data->BaseAddress);
info.size = static_cast<uint64_t>(data->Size);
g_moduleCollector.dest->append(info);
}
// We still need a section callback even though we don't use it.
void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*)
{
// Intentionally empty -- we only need module data.
}
} // anonymous namespace
void RcNetCompatProvider::cacheModules()
{
if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle)
return;
m_modules.clear();
g_moduleCollector.dest = &m_modules;
m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback);
g_moduleCollector.dest = nullptr;
// Set base to first module if we got any
if (!m_modules.isEmpty() && m_base == 0)
m_base = m_modules.first().base;
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include "../../src/providers/provider.h"
#include "ReClassNET_Plugin.hpp"
#include <QString>
#include <QVector>
/**
* Provider that bridges ReClass.NET native plugin DLL calls
* to the ReclassX Provider interface.
*/
class RcNetCompatProvider : public rcx::Provider
{
public:
RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid,
const QString& processName);
~RcNetCompatProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
// Optional overrides
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("RcNet"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
struct ModuleInfo {
QString name;
uint64_t base;
uint64_t size;
};
private:
void cacheModules();
RcNetFunctions m_fns;
RC_Pointer m_handle = nullptr;
uint32_t m_pid;
QString m_processName;
uint64_t m_base = 0;
QVector<ModuleInfo> m_modules;
};

View File

@@ -0,0 +1,140 @@
#pragma once
// Subset of ReClass.NET native plugin types needed for the compatibility layer.
// Based on the ReClass.NET NativeCore plugin interface.
// Only types required by the 8 supported exports are included (no debug types).
#include <cstdint>
#ifdef _WIN32
#define RC_CALLCONV __stdcall
#else
#define RC_CALLCONV
#endif
// -- Basic types ----------------------------------------------------------
using RC_Pointer = void*;
using RC_Size = uint64_t;
using RC_UnicodeChar = char16_t;
// -- Enums ----------------------------------------------------------------
enum class ProcessAccess
{
Read = 0,
Write = 1,
Full = 2
};
enum class SectionProtection
{
NoAccess = 0,
Read = 1,
Write = 2,
Execute = 4,
Guard = 8
};
enum class SectionType
{
Unknown = 0,
Private = 1,
Mapped = 2,
Image = 3
};
enum class SectionCategory
{
Unknown = 0,
CODE = 1,
DATA = 2,
HEAP = 3
};
enum class ControlRemoteProcessAction
{
Suspend = 0,
Resume = 1,
Terminate = 2
};
// -- Callback data structures ---------------------------------------------
#pragma pack(push, 1)
struct EnumerateProcessData
{
RC_Size Id;
RC_UnicodeChar Name[260];
RC_UnicodeChar Path[260];
};
struct EnumerateRemoteSectionData
{
RC_Pointer BaseAddress;
RC_Size Size;
SectionType Type;
SectionCategory Category;
SectionProtection Protection;
RC_UnicodeChar Name[16];
RC_UnicodeChar ModulePath[260];
};
struct EnumerateRemoteModuleData
{
RC_Pointer BaseAddress;
RC_Size Size;
RC_UnicodeChar Path[260];
};
#pragma pack(pop)
// -- Callback typedefs ----------------------------------------------------
using EnumerateProcessCallback = void(RC_CALLCONV*)(EnumerateProcessData* data);
using EnumerateRemoteSectionsCallback = void(RC_CALLCONV*)(EnumerateRemoteSectionData* data);
using EnumerateRemoteModulesCallback = void(RC_CALLCONV*)(EnumerateRemoteModuleData* data);
// -- Function pointer typedefs for resolved exports -----------------------
using FnEnumerateProcesses = void(RC_CALLCONV*)(EnumerateProcessCallback callback);
using FnOpenRemoteProcess = RC_Pointer(RC_CALLCONV*)(RC_Size id, ProcessAccess desiredAccess);
using FnIsProcessValid = bool(RC_CALLCONV*)(RC_Pointer handle);
using FnCloseRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle);
using FnReadRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
RC_Pointer address,
RC_Pointer buffer,
int offset,
int size);
using FnWriteRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
RC_Pointer address,
RC_Pointer buffer,
int offset,
int size);
using FnEnumerateRemoteSectionsAndModules =
void(RC_CALLCONV*)(RC_Pointer handle,
EnumerateRemoteSectionsCallback sectionCallback,
EnumerateRemoteModulesCallback moduleCallback);
using FnControlRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle,
ControlRemoteProcessAction action);
// -- Resolved function table ----------------------------------------------
struct RcNetFunctions
{
FnEnumerateProcesses EnumerateProcesses = nullptr;
FnOpenRemoteProcess OpenRemoteProcess = nullptr;
FnIsProcessValid IsProcessValid = nullptr;
FnCloseRemoteProcess CloseRemoteProcess = nullptr;
FnReadRemoteMemory ReadRemoteMemory = nullptr;
FnWriteRemoteMemory WriteRemoteMemory = nullptr;
FnEnumerateRemoteSectionsAndModules EnumerateRemoteSectionsAndModules = nullptr;
FnControlRemoteProcess ControlRemoteProcess = nullptr;
};

View File

@@ -0,0 +1,677 @@
// RcNetBridge -- in-process C# bridge for loading .NET ReClass.NET plugins.
//
// Called from C++ via ICLRRuntimeHost::ExecuteInDefaultAppDomain().
// The single entry point is Bridge.Initialize(string arg) where arg is:
// "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
//
// The bridge:
// 1. Registers an AssemblyResolve handler that provides THIS assembly
// when a plugin asks for "ReClassNET", so the stub types below satisfy
// the plugin's type references.
// 2. Loads the plugin assembly and finds an ICoreProcessFunctions
// implementation.
// 3. Creates [UnmanagedFunctionPointer] delegates wrapping each method.
// 4. Writes the native-callable function pointers into the RcNetFunctions
// struct at the address provided by C++.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
// ===========================================================================
// ReClass.NET stub types
// These mirror the subset of types from the ReClass.NET assembly that
// memory-reading plugins reference. When the CLR resolves "ReClassNET"
// via our AssemblyResolve handler, it gets THIS assembly, and these types
// satisfy the plugin's type references.
//
// Types are placed in the exact namespaces used by the real ReClass.NET
// assembly so that plugins compiled against it resolve correctly.
// ===========================================================================
// --------------------------------------------------------------------------
// ReClassNET.Memory -- section enums (referenced by EnumerateRemoteSectionData)
// --------------------------------------------------------------------------
namespace ReClassNET.Memory
{
public enum SectionProtection
{
NoAccess = 0,
Read = 1,
Write = 2,
Execute = 4,
Guard = 8
}
public enum SectionType
{
Unknown = 0,
Private = 1,
Mapped = 2,
Image = 3
}
public enum SectionCategory
{
Unknown = 0,
CODE = 1,
DATA = 2,
HEAP = 3
}
}
// --------------------------------------------------------------------------
// ReClassNET.Debugger -- debugger types (used by ICoreProcessFunctions)
// --------------------------------------------------------------------------
namespace ReClassNET.Debugger
{
public enum DebugContinueStatus
{
Handled = 0,
NotHandled = 1
}
public enum HardwareBreakpointRegister
{
InvalidRegister = 0,
Dr0 = 1,
Dr1 = 2,
Dr2 = 3,
Dr3 = 4
}
public enum HardwareBreakpointTrigger
{
Execute = 0,
Access = 1,
Write = 2
}
public enum HardwareBreakpointSize
{
Size1 = 1,
Size2 = 2,
Size4 = 4,
Size8 = 8
}
public struct ExceptionDebugInfo
{
public IntPtr ExceptionCode;
public IntPtr ExceptionFlags;
public IntPtr ExceptionAddress;
public HardwareBreakpointRegister CausedBy;
public RegisterInfo Registers;
public struct RegisterInfo
{
public IntPtr Rax, Rbx, Rcx, Rdx;
public IntPtr Rdi, Rsi, Rsp, Rbp, Rip;
public IntPtr R8, R9, R10, R11, R12, R13, R14, R15;
}
}
public struct DebugEvent
{
public DebugContinueStatus ContinueStatus;
public IntPtr ProcessId;
public IntPtr ThreadId;
public ExceptionDebugInfo ExceptionInfo;
}
}
// --------------------------------------------------------------------------
// ReClassNET.Core -- interface, enums, delegates, and data structs
// --------------------------------------------------------------------------
namespace ReClassNET.Core
{
public enum ProcessAccess
{
Read = 0,
Write = 1,
Full = 2
}
public enum ControlRemoteProcessAction
{
Suspend = 0,
Resume = 1,
Terminate = 2
}
public struct EnumerateProcessData
{
public IntPtr Id;
public string Name;
public string Path;
}
public struct EnumerateRemoteSectionData
{
public IntPtr BaseAddress;
public IntPtr Size;
public ReClassNET.Memory.SectionType Type;
public ReClassNET.Memory.SectionCategory Category;
public ReClassNET.Memory.SectionProtection Protection;
public string Name;
public string ModulePath;
}
public struct EnumerateRemoteModuleData
{
public IntPtr BaseAddress;
public IntPtr Size;
public string Path;
}
public delegate void EnumerateProcessCallback(ref EnumerateProcessData data);
public delegate void EnumerateRemoteSectionCallback(ref EnumerateRemoteSectionData data);
public delegate void EnumerateRemoteModuleCallback(ref EnumerateRemoteModuleData data);
public interface ICoreProcessFunctions
{
void EnumerateProcesses(EnumerateProcessCallback callbackProcess);
IntPtr OpenRemoteProcess(IntPtr pid, ProcessAccess desiredAccess);
bool IsProcessValid(IntPtr process);
void CloseRemoteProcess(IntPtr process);
bool ReadRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
bool WriteRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
void EnumerateRemoteSectionsAndModules(
IntPtr process,
EnumerateRemoteSectionCallback callbackSection,
EnumerateRemoteModuleCallback callbackModule);
void ControlRemoteProcess(IntPtr process, ControlRemoteProcessAction action);
// Debugger methods -- stubs required for interface compatibility
bool AttachDebuggerToProcess(IntPtr id);
void DetachDebuggerFromProcess(IntPtr id);
bool AwaitDebugEvent(ref ReClassNET.Debugger.DebugEvent evt, int timeoutInMilliseconds);
void HandleDebugEvent(ref ReClassNET.Debugger.DebugEvent evt);
bool SetHardwareBreakpoint(IntPtr id, IntPtr address,
ReClassNET.Debugger.HardwareBreakpointRegister register,
ReClassNET.Debugger.HardwareBreakpointTrigger trigger,
ReClassNET.Debugger.HardwareBreakpointSize size,
bool set);
}
}
// --------------------------------------------------------------------------
// ReClassNET.Memory -- RemoteProcess stub
// --------------------------------------------------------------------------
namespace ReClassNET.Memory
{
public class RemoteProcess { }
}
// --------------------------------------------------------------------------
// ReClassNET.Logger -- ILogger stub
// --------------------------------------------------------------------------
namespace ReClassNET.Logger
{
public interface ILogger { }
}
// --------------------------------------------------------------------------
// Stub types for IPluginHost properties
// --------------------------------------------------------------------------
namespace ReClassNET.Forms
{
public class MainForm { }
}
namespace ReClassNET
{
public class Settings { }
}
// --------------------------------------------------------------------------
// ReClassNET.Plugins
// --------------------------------------------------------------------------
namespace ReClassNET.Plugins
{
public abstract class Plugin : IDisposable
{
public virtual bool Initialize(IPluginHost host) { return true; }
public virtual void Terminate() { }
public virtual void Dispose() { }
}
public interface IPluginHost
{
ReClassNET.Forms.MainForm MainWindow { get; }
System.Resources.ResourceManager Resources { get; }
ReClassNET.Memory.RemoteProcess Process { get; }
ReClassNET.Logger.ILogger Logger { get; }
ReClassNET.Settings Settings { get; }
}
}
// ===========================================================================
// Bridge
// ===========================================================================
namespace RcNetBridge
{
internal class StubPluginHost : ReClassNET.Plugins.IPluginHost
{
public ReClassNET.Forms.MainForm MainWindow => null;
public System.Resources.ResourceManager Resources => null;
public ReClassNET.Memory.RemoteProcess Process => null;
public ReClassNET.Logger.ILogger Logger => null;
public ReClassNET.Settings Settings => null;
}
public class Bridge
{
// -- Persistent state (static so it survives after Initialize returns) --
private static ReClassNET.Core.ICoreProcessFunctions s_functions;
private static readonly List<Delegate> s_pinned = new List<Delegate>();
// -- Entry point called from C++ --------------------------------------
/// <summary>
/// Called by ICLRRuntimeHost::ExecuteInDefaultAppDomain.
/// arg = "&lt;hex_address_of_RcNetFunctions&gt;|&lt;plugin_dll_path&gt;"
/// Returns 0 on success, non-zero error code on failure.
/// </summary>
public static int Initialize(string arg)
{
try
{
int sep = arg.IndexOf('|');
if (sep < 0) return 1; // bad arg
long ptrValue = long.Parse(arg.Substring(0, sep), NumberStyles.HexNumber);
IntPtr funcTablePtr = new IntPtr(ptrValue);
string pluginPath = arg.Substring(sep + 1);
// Set up assembly resolution
string pluginDir = Path.GetDirectoryName(pluginPath) ?? ".";
string parentDir = Path.GetDirectoryName(pluginDir);
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
{
string asmName = new AssemblyName(resolveArgs.Name).Name;
// Provide our own assembly as the "ReClass.NET" stub
if (string.Equals(asmName, "ReClass.NET", StringComparison.OrdinalIgnoreCase))
return typeof(Bridge).Assembly;
// Search plugin directory and parent for other dependencies
string dllName = asmName + ".dll";
foreach (string dir in new[] { pluginDir, parentDir })
{
if (dir == null) continue;
string path = Path.Combine(dir, dllName);
if (File.Exists(path))
return Assembly.LoadFrom(path);
}
return null;
};
// Load plugin and find ICoreProcessFunctions
if (!LoadPlugin(pluginPath))
return 2; // no implementation found
// Write function pointers
WriteFunctionPointers(funcTablePtr);
return 0;
}
catch (Exception ex) when (ex is ReflectionTypeLoadException || ex is FileNotFoundException)
{
return 3;
}
catch
{
return 4;
}
}
// -- Plugin loading ---------------------------------------------------
private static bool LoadPlugin(string pluginPath)
{
Assembly asm = Assembly.LoadFrom(pluginPath);
// Find a concrete type that implements ICoreProcessFunctions.
// ReClass.NET plugins typically extend Plugin and directly
// implement ICoreProcessFunctions on the same class.
foreach (Type type in asm.GetExportedTypes())
{
if (type.IsAbstract || type.IsInterface) continue;
Type iface = type.GetInterfaces().FirstOrDefault(i =>
i.FullName == "ReClassNET.Core.ICoreProcessFunctions");
if (iface == null) continue;
object instance = Activator.CreateInstance(type);
// Try calling Initialize() but don't fail if it throws --
// plugins use it for UI integration with the host app,
// which we can't fully provide. The process functions
// (ReadRemoteMemory, etc.) work without it.
try
{
MethodInfo init = type.GetMethod("Initialize",
BindingFlags.Public | BindingFlags.Instance,
null, new[] { typeof(ReClassNET.Plugins.IPluginHost) }, null);
if (init != null)
init.Invoke(instance, new object[] { new StubPluginHost() });
}
catch { }
s_functions = (ReClassNET.Core.ICoreProcessFunctions)instance;
return true;
}
return false;
}
// -- Native-callable delegate types -----------------------------------
// These match the C++ RcNetFunctions struct field order exactly.
// On x64 Windows all calling conventions collapse to the Microsoft
// x64 ABI, so StdCall is used for documentation / x86 correctness.
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void DelEnumProcesses(IntPtr callback);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate IntPtr DelOpenRemoteProcess(ulong id, int access);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.I1)]
delegate bool DelIsProcessValid(IntPtr handle);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void DelCloseRemoteProcess(IntPtr handle);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.I1)]
delegate bool DelReadRemoteMemory(IntPtr handle, IntPtr address,
IntPtr buffer, int offset, int size);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.I1)]
delegate bool DelWriteRemoteMemory(IntPtr handle, IntPtr address,
IntPtr buffer, int offset, int size);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void DelEnumSectionsAndModules(IntPtr handle,
IntPtr sectionCallback, IntPtr moduleCallback);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void DelControlRemoteProcess(IntPtr handle, int action);
// Callback delegate types -- these point into C++ and are called by us.
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void NativeProcessCallback(IntPtr data);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void NativeSectionCallback(IntPtr data);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void NativeModuleCallback(IntPtr data);
// -- Write function pointers to the C++ struct ------------------------
private static void WriteFunctionPointers(IntPtr funcTable)
{
// RcNetFunctions layout: 8 consecutive function pointers.
int i = 0;
WriteSlot(funcTable, i++, Pin<DelEnumProcesses>(EnumProcessesImpl));
WriteSlot(funcTable, i++, Pin<DelOpenRemoteProcess>(OpenProcessImpl));
WriteSlot(funcTable, i++, Pin<DelIsProcessValid>(IsProcessValidImpl));
WriteSlot(funcTable, i++, Pin<DelCloseRemoteProcess>(CloseProcessImpl));
WriteSlot(funcTable, i++, Pin<DelReadRemoteMemory>(ReadMemoryImpl));
WriteSlot(funcTable, i++, Pin<DelWriteRemoteMemory>(WriteMemoryImpl));
WriteSlot(funcTable, i++, Pin<DelEnumSectionsAndModules>(EnumSectionsModulesImpl));
WriteSlot(funcTable, i++, Pin<DelControlRemoteProcess>(ControlProcessImpl));
}
private static IntPtr Pin<T>(T del) where T : class
{
Delegate d = del as Delegate;
s_pinned.Add(d); // prevent GC
return Marshal.GetFunctionPointerForDelegate(d);
}
private static void WriteSlot(IntPtr table, int index, IntPtr value)
{
Marshal.WriteIntPtr(table, index * IntPtr.Size, value);
}
// -- Implementation methods -------------------------------------------
// -- EnumerateProcesses --
// C++ passes a native callback; we call the plugin, convert each
// managed EnumerateProcessData to the packed native layout, and
// forward to the native callback.
private static void EnumProcessesImpl(IntPtr nativeCallbackPtr)
{
try
{
if (s_functions == null || nativeCallbackPtr == IntPtr.Zero) return;
NativeProcessCallback nativeCb =
Marshal.GetDelegateForFunctionPointer<NativeProcessCallback>(nativeCallbackPtr);
// Native layout (pack=1): uint64 Id + char16[260] Name + char16[260] Path
const int kStructSize = 8 + 520 + 520; // 1048 bytes
s_functions.EnumerateProcesses(
(ref ReClassNET.Core.EnumerateProcessData data) =>
{
IntPtr mem = Marshal.AllocHGlobal(kStructSize);
try
{
// Zero-fill
byte[] zeros = new byte[kStructSize];
Marshal.Copy(zeros, 0, mem, kStructSize);
// Id (8 bytes at offset 0)
Marshal.WriteInt64(mem, 0, data.Id.ToInt64());
// Name (char16[260] at offset 8)
if (data.Name != null)
{
char[] chars = data.Name.ToCharArray();
int count = Math.Min(chars.Length, 259);
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 8), count);
}
// Path (char16[260] at offset 528)
if (data.Path != null)
{
char[] chars = data.Path.ToCharArray();
int count = Math.Min(chars.Length, 259);
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 528), count);
}
nativeCb(mem);
}
finally
{
Marshal.FreeHGlobal(mem);
}
});
}
catch { /* swallow -- don't crash the host process */ }
}
// -- OpenRemoteProcess --
private static IntPtr OpenProcessImpl(ulong id, int access)
{
try
{
if (s_functions == null) return IntPtr.Zero;
return s_functions.OpenRemoteProcess(
new IntPtr((long)id),
(ReClassNET.Core.ProcessAccess)access);
}
catch { return IntPtr.Zero; }
}
// -- IsProcessValid --
private static bool IsProcessValidImpl(IntPtr handle)
{
try
{
if (s_functions == null) return false;
return s_functions.IsProcessValid(handle);
}
catch { return false; }
}
// -- CloseRemoteProcess --
private static void CloseProcessImpl(IntPtr handle)
{
try { s_functions?.CloseRemoteProcess(handle); }
catch { }
}
// -- ReadRemoteMemory --
// C++ provides a native buffer pointer. We read into a managed array
// via the plugin's interface, then copy to the native buffer.
private static bool ReadMemoryImpl(IntPtr handle, IntPtr address,
IntPtr buffer, int offset, int size)
{
try
{
if (s_functions == null || size <= 0) return false;
byte[] managed = new byte[size];
bool ok = s_functions.ReadRemoteMemory(
handle, address, ref managed, 0, size);
if (ok)
Marshal.Copy(managed, 0, new IntPtr(buffer.ToInt64() + offset), size);
return ok;
}
catch { return false; }
}
// -- WriteRemoteMemory --
private static bool WriteMemoryImpl(IntPtr handle, IntPtr address,
IntPtr buffer, int offset, int size)
{
try
{
if (s_functions == null || size <= 0) return false;
byte[] managed = new byte[size];
Marshal.Copy(new IntPtr(buffer.ToInt64() + offset), managed, 0, size);
return s_functions.WriteRemoteMemory(
handle, address, ref managed, 0, size);
}
catch { return false; }
}
// -- EnumerateRemoteSectionsAndModules --
private static void EnumSectionsModulesImpl(IntPtr handle,
IntPtr sectionCallbackPtr, IntPtr moduleCallbackPtr)
{
try
{
if (s_functions == null) return;
// Section callback -- forward to native
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
// SectionType(4) + SectionCategory(4) + SectionProtection(4) +
// char16 Name[16](32) + char16 ModulePath[260](520) = 580 bytes
NativeSectionCallback nativeSectionCb = (sectionCallbackPtr != IntPtr.Zero)
? Marshal.GetDelegateForFunctionPointer<NativeSectionCallback>(sectionCallbackPtr)
: null;
// Module callback -- forward to native
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
// char16 Path[260](520) = 536 bytes
NativeModuleCallback nativeModuleCb = (moduleCallbackPtr != IntPtr.Zero)
? Marshal.GetDelegateForFunctionPointer<NativeModuleCallback>(moduleCallbackPtr)
: null;
s_functions.EnumerateRemoteSectionsAndModules(handle,
// Section callback
(ref ReClassNET.Core.EnumerateRemoteSectionData sdata) =>
{
if (nativeSectionCb == null) return;
const int kSize = 8 + 8 + 4 + 4 + 4 + 32 + 520; // 580
IntPtr mem = Marshal.AllocHGlobal(kSize);
try
{
byte[] z = new byte[kSize];
Marshal.Copy(z, 0, mem, kSize);
Marshal.WriteInt64(mem, 0, sdata.BaseAddress.ToInt64());
Marshal.WriteInt64(mem, 8, sdata.Size.ToInt64());
Marshal.WriteInt32(mem, 16, (int)sdata.Type);
Marshal.WriteInt32(mem, 20, (int)sdata.Category);
Marshal.WriteInt32(mem, 24, (int)sdata.Protection);
if (sdata.Name != null)
{
char[] c = sdata.Name.ToCharArray();
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 28),
Math.Min(c.Length, 15));
}
if (sdata.ModulePath != null)
{
char[] c = sdata.ModulePath.ToCharArray();
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 60),
Math.Min(c.Length, 259));
}
nativeSectionCb(mem);
}
finally { Marshal.FreeHGlobal(mem); }
},
// Module callback
(ref ReClassNET.Core.EnumerateRemoteModuleData mdata) =>
{
if (nativeModuleCb == null) return;
const int kSize = 8 + 8 + 520; // 536
IntPtr mem = Marshal.AllocHGlobal(kSize);
try
{
byte[] z = new byte[kSize];
Marshal.Copy(z, 0, mem, kSize);
Marshal.WriteInt64(mem, 0, mdata.BaseAddress.ToInt64());
Marshal.WriteInt64(mem, 8, mdata.Size.ToInt64());
if (mdata.Path != null)
{
char[] c = mdata.Path.ToCharArray();
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 16),
Math.Min(c.Length, 259));
}
nativeModuleCb(mem);
}
finally { Marshal.FreeHGlobal(mem); }
});
}
catch { }
}
// -- ControlRemoteProcess --
private static void ControlProcessImpl(IntPtr handle, int action)
{
try
{
s_functions?.ControlRemoteProcess(handle,
(ReClassNET.Core.ControlRemoteProcessAction)action);
}
catch { }
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<AssemblyName>RcNetBridge</AssemblyName>
<RootNamespace>RcNetBridge</RootNamespace>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<LangVersion>7.3</LangVersion>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,124 @@
cmake_minimum_required(VERSION 3.20)
project(RemoteProcessMemory LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin
# ─── 1. Payload DLL/SO (no Qt, minimal dependencies) ────────────────
add_library(rcx_payload SHARED
payload/rcx_payload.cpp
rcx_rpc_protocol.h
)
set_target_properties(rcx_payload PROPERTIES PREFIX "") # rcx_payload.dll / rcx_payload.so
target_include_directories(rcx_payload PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_link_libraries(rcx_payload PRIVATE psapi)
else()
target_link_libraries(rcx_payload PRIVATE pthread rt)
target_compile_options(rcx_payload PRIVATE -fvisibility=hidden)
endif()
# Output payload to Plugins/ (same dir as plugin DLL, discovered at runtime)
set_target_properties(rcx_payload PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
# Install rule: copy both DLLs to install Plugins/ folder
install(TARGETS rcx_payload
LIBRARY DESTINATION Plugins
RUNTIME DESTINATION Plugins
)
# ─── 2. Plugin DLL (Qt, implements IProviderPlugin) ──────────────────
# Generate ui_processpicker.h in our own build dir (avoids dupbuild with ProcessMemoryPlugin)
set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui")
set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h")
add_custom_command(
OUTPUT "${_UI_HDR}"
COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}"
DEPENDS "${_UI_SRC}"
COMMENT "UIC processpicker.ui (RemoteProcessMemory)"
VERBATIM
)
set(PLUGIN_SOURCES
RemoteProcessMemoryPlugin.h
RemoteProcessMemoryPlugin.cpp
rcx_rpc_protocol.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
"${_UI_HDR}"
)
add_library(RemoteProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE
${QT}::Widgets
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE psapi shell32)
else()
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE rt dl)
target_compile_options(RemoteProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
target_include_directories(RemoteProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
)
set_target_properties(RemoteProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
install(TARGETS RemoteProcessMemoryPlugin
LIBRARY DESTINATION Plugins
RUNTIME DESTINATION Plugins
)
# Plugin must be able to find the payload at runtime
add_dependencies(RemoteProcessMemoryPlugin rcx_payload)
# ─── 3. Test executables (no Qt) ────────────────────────────────────
# Host: loads payload in-process, exposes test buffer
add_executable(test_rpc_host tests/test_rpc_host.cpp)
target_include_directories(test_rpc_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_link_libraries(test_rpc_host PRIVATE psapi)
else()
target_link_libraries(test_rpc_host PRIVATE pthread rt dl)
endif()
set_target_properties(test_rpc_host PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
add_dependencies(test_rpc_host rcx_payload)
# Client: connects to host, tests + benchmarks
add_executable(test_rpc_client tests/test_rpc_client.cpp)
target_include_directories(test_rpc_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_link_libraries(test_rpc_client PRIVATE psapi)
else()
target_link_libraries(test_rpc_client PRIVATE pthread rt)
endif()
set_target_properties(test_rpc_client PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
add_dependencies(test_rpc_client test_rpc_host)

View File

@@ -0,0 +1,939 @@
#include "RemoteProcessMemoryPlugin.h"
#include "rcx_rpc_protocol.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QPushButton>
#include <QDir>
#include <QFileInfo>
#include <QPixmap>
#include <QImage>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
#include <QtWin>
#endif
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <tlhelp32.h>
# include <psapi.h>
# include <shellapi.h>
#else
# include <unistd.h>
# include <fcntl.h>
# include <dlfcn.h>
# include <sys/mman.h>
# include <sys/wait.h>
# include <sys/ptrace.h>
# include <sys/user.h>
# include <semaphore.h>
# include <signal.h>
# include <link.h>
# include <climits>
# include <cstring>
# include <fstream>
# include <sstream>
#endif
/* ══════════════════════════════════════════════════════════════════════
* IPC Client
* ══════════════════════════════════════════════════════════════════════ */
struct IpcClient {
#ifdef _WIN32
HANDLE hShm = nullptr;
HANDLE hReqEvent = nullptr;
HANDLE hRspEvent = nullptr;
#else
int shmFd = -1;
sem_t* reqSem = SEM_FAILED;
sem_t* rspSem = SEM_FAILED;
char shmNameBuf[128] = {};
char reqNameBuf[128] = {};
char rspNameBuf[128] = {};
#endif
void* mappedView = nullptr;
QMutex mutex;
bool connected = false;
RcxRpcHeader* header() const {
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
}
~IpcClient() { disconnect(); }
/* ── connect / disconnect ──────────────────────────────────────── */
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
/* poll for shared memory to appear (payload creating it) */
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
if (GetTickCount64() >= deadline) return false;
Sleep(10);
}
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
#else
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
/* poll for shared memory */
auto start = std::chrono::steady_clock::now();
while (true) {
shmFd = shm_open(shmName, O_RDWR, 0);
if (shmFd >= 0) break;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(10000);
}
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shmFd, 0);
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
reqSem = sem_open(reqName, 0);
rspSem = sem_open(rspName, 0);
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
#endif
/* wait for payloadReady */
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
#ifdef _WIN32
while (!hdr->payloadReady) {
if (GetTickCount64() >= deadline) { disconnect(); return false; }
Sleep(5);
}
#else
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) { disconnect(); return false; }
usleep(5000);
}
#endif
connected = true;
return true;
}
void disconnect()
{
#ifdef _WIN32
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
#else
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
#endif
connected = false;
}
/* ── low-level RPC round-trip ──────────────────────────────────── */
bool signalAndWait(int timeoutMs = 2000)
{
#ifdef _WIN32
SetEvent(hReqEvent);
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
#else
sem_post(reqSem);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeoutMs / 1000;
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
return sem_timedwait(rspSem, &ts) == 0;
#endif
}
/* ── public API ────────────────────────────────────────────────── */
bool readSingle(uint64_t addr, void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = 1;
hdr->status = RCX_RPC_STATUS_OK;
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
entry->address = addr;
entry->length = (uint32_t)len;
entry->dataOffset = sizeof(RcxRpcReadEntry);
if (!signalAndWait()) { connected = false; return false; }
memcpy(buf, data + entry->dataOffset, len);
return true;
}
bool writeSingle(uint64_t addr, const void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_WRITE;
hdr->writeAddress = addr;
hdr->writeLength = (uint32_t)len;
hdr->status = RCX_RPC_STATUS_OK;
memcpy(data, buf, len);
if (!signalAndWait()) { connected = false; return false; }
return hdr->status == RCX_RPC_STATUS_OK;
}
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
{
QVector<RemoteProcessProvider::ModuleInfo> result;
QMutexLocker lock(&mutex);
if (!connected) return result;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_ENUM_MODULES;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return result; }
if (hdr->status != RCX_RPC_STATUS_OK) return result;
uint32_t count = hdr->responseCount;
result.reserve((int)count);
for (uint32_t i = 0; i < count; ++i) {
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
data + i * sizeof(RcxRpcModuleEntry));
QString modName;
#ifdef _WIN32
modName = QString::fromWCharArray(
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
(int)(entry->nameLength / sizeof(wchar_t)));
#else
modName = QString::fromUtf8(
reinterpret_cast<const char*>(data + entry->nameOffset),
(int)entry->nameLength);
#endif
result.append({modName, entry->base, entry->size});
}
return result;
}
bool ping()
{
QMutexLocker lock(&mutex);
if (!connected) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_PING;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return false; }
return true;
}
void shutdown()
{
QMutexLocker lock(&mutex);
if (!connected) return;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_SHUTDOWN;
hdr->status = RCX_RPC_STATUS_OK;
signalAndWait(500);
connected = false;
}
};
/* ══════════════════════════════════════════════════════════════════════
* RemoteProcessProvider
* ══════════════════════════════════════════════════════════════════════ */
RemoteProcessProvider::RemoteProcessProvider(
uint32_t pid, const QString& processName,
std::shared_ptr<IpcClient> ipc)
: m_pid(pid)
, m_processName(processName)
, m_connected(ipc && ipc->connected)
, m_base(0)
, m_ipc(std::move(ipc))
{
if (m_connected) {
cacheModules();
// Read pointer size from payload's SHM header (0 means not set → default 8)
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
if (hdr) {
uint32_t ps = hdr->pointerSize;
if (ps == 4 || ps == 8)
m_pointerSize = (int)ps;
}
}
}
RemoteProcessProvider::~RemoteProcessProvider() = default;
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->readSingle(addr, buf, len);
if (!ok) {
memset(buf, 0, (size_t)len);
/* update connectivity flag through mutable ipc */
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
}
return ok;
}
int RemoteProcessProvider::size() const
{
return m_connected ? 0x10000 : 0;
}
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->writeSingle(addr, buf, len);
if (!ok) m_connected = m_ipc->connected;
return ok;
}
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules) {
if (addr >= mod.base && addr < mod.base + mod.size) {
uint64_t off = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(off, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
void RemoteProcessProvider::cacheModules()
{
m_modules = m_ipc->enumerateModules();
if (!m_modules.isEmpty())
m_base = m_modules.first().base;
}
/* ══════════════════════════════════════════════════════════════════════
* Injection helpers
* ══════════════════════════════════════════════════════════════════════ */
namespace {
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
static QString payloadPath()
{
#ifdef _WIN32
HMODULE hSelf = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
WCHAR buf[MAX_PATH];
GetModuleFileNameW(hSelf, buf, MAX_PATH);
QFileInfo fi(QString::fromWCharArray(buf));
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
#else
Dl_info info;
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
QFileInfo fi(QString::fromUtf8(info.dli_fname));
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
#endif
}
#ifdef _WIN32
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
static bool injectPayload(uint32_t pid, QString* errorMsg)
{
QString path = payloadPath();
QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit();
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProc) {
if (errorMsg)
*errorMsg = QStringLiteral("OpenProcess failed (error %1).\n"
"Try running as Administrator.")
.arg(GetLastError());
return false;
}
/* allocate + write path string in target */
SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1);
void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remotePath) {
if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed.");
CloseHandle(hProc);
return false;
}
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(hK32, "LoadLibraryA"));
HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0,
pLoadLib, remotePath, 0, nullptr);
if (!hThread) {
if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).")
.arg(GetLastError());
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
CloseHandle(hProc);
return false;
}
WaitForSingleObject(hThread, 10000);
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
CloseHandle(hThread);
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
if (exitCode == 0) {
CloseHandle(hProc);
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
"Ensure rcx_payload.dll is in: %1").arg(path);
return false;
}
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
(loader lock is no longer held after LoadLibrary returned) */
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
GetProcAddress(hK32, "GetProcAddress"));
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
const char initName[] = "RcxPayloadInit";
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteInitName) {
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
Simpler approach: write small shellcode that does both calls. */
uint8_t shellcode[128];
int off = 0;
/* sub rsp, 40 ; shadow space + alignment */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
/* mov rcx, hPayloadRemote ; first arg = module handle */
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
memcpy(shellcode + off, &hMod, 8); off += 8;
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
memcpy(shellcode + off, &pName, 8); off += 8;
/* mov rax, GetProcAddress */
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
memcpy(shellcode + off, &pGPA, 8); off += 8;
/* call rax ; rax = RcxPayloadInit */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* test rax, rax */
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
/* jz skip (jump over the call if null) */
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
/* call rax ; RcxPayloadInit() */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* skip: add rsp, 40 */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
/* ret */
shellcode[off++] = 0xC3;
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (remoteCode) {
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
if (hThread2) {
WaitForSingleObject(hThread2, 10000);
CloseHandle(hThread2);
}
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
}
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
}
CloseHandle(hProc);
return true;
}
#else
/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */
static uint64_t findLibBase(pid_t pid, const char* libName)
{
char mapsPath[64];
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
FILE* f = fopen(mapsPath, "r");
if (!f) return 0;
char line[1024];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, libName)) {
uint64_t base;
if (sscanf(line, "%lx-", &base) == 1) {
fclose(f);
return base;
}
}
}
fclose(f);
return 0;
}
static uint64_t findSyscallInsn(pid_t pid)
{
char mapsPath[64];
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
FILE* f = fopen(mapsPath, "r");
if (!f) return 0;
char line[1024];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "libc") && strstr(line, "r-xp")) {
uint64_t start, end;
if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
fclose(f);
/* scan for 0F 05 (syscall) */
char memPath[64];
snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid);
int memFd = open(memPath, O_RDONLY);
if (memFd < 0) return 0;
uint8_t buf[4096];
for (uint64_t off = start; off < end; off += sizeof(buf)) {
ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off);
if (n <= 1) break;
for (ssize_t i = 0; i + 1 < n; ++i) {
if (buf[i] == 0x0F && buf[i + 1] == 0x05) {
close(memFd);
return off + (uint64_t)i;
}
}
}
close(memFd);
return 0;
}
}
fclose(f);
return 0;
}
static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len)
{
const uint8_t* p = static_cast<const uint8_t*>(src);
for (size_t i = 0; i < len; i += sizeof(long)) {
long val = 0;
size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long);
if (chunk < sizeof(long)) {
errno = 0;
val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr);
if (errno) return false;
}
memcpy(&val, p + i, chunk);
if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0)
return false;
}
return true;
}
static bool injectPayload(uint32_t pid, QString* errorMsg)
{
QString path = payloadPath();
QByteArray pathUtf8 = path.toUtf8();
if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) {
if (errorMsg)
*errorMsg = QStringLiteral("ptrace attach failed: %1\n"
"Check /proc/sys/kernel/yama/ptrace_scope or run as root.")
.arg(strerror(errno));
return false;
}
int status;
waitpid((pid_t)pid, &status, 0);
/* save registers */
struct user_regs_struct savedRegs, regs;
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs);
regs = savedRegs;
/* find syscall instruction in target's libc */
uint64_t syscallAddr = findSyscallInsn((pid_t)pid);
if (!syscallAddr) {
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target.");
return false;
}
/* find dlopen in target via libc offset technique */
void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen");
uint64_t ourLibcBase = findLibBase(getpid(), "libc");
uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc");
if (!ourDlopen || !ourLibcBase || !targetLibcBase) {
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address.");
return false;
}
uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase);
/* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */
regs.rax = 9; /* __NR_mmap */
regs.rdi = 0;
regs.rsi = 4096;
regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */
regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */
regs.r8 = (uint64_t)-1;
regs.r9 = 0;
regs.rip = syscallAddr;
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &regs);
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
waitpid((pid_t)pid, &status, 0);
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &regs);
uint64_t mmapPage = regs.rax;
if ((int64_t)mmapPage < 0 || mmapPage == 0) {
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (errorMsg) *errorMsg = QStringLiteral("mmap in target failed.");
return false;
}
/* write path string at start of page */
writeTargetMem((pid_t)pid, mmapPage, pathUtf8.constData(), (size_t)(pathUtf8.size() + 1));
/* write shellcode after path:
* mov rdi, pathAddr (48 BF xxxxxxxx)
* mov rsi, 2 (48 BE 02000000 00000000)
* mov rax, dlopenAddr (48 B8 xxxxxxxx)
* call rax (FF D0)
* int3 (CC)
*/
uint64_t pathAddr = mmapPage;
uint64_t codeAddr = mmapPage + ((pathUtf8.size() + 1 + 15) & ~15ULL);
uint8_t sc[64];
int len = 0;
/* mov rdi, imm64 */
sc[len++] = 0x48; sc[len++] = 0xBF;
memcpy(sc + len, &pathAddr, 8); len += 8;
/* mov rsi, 2 (RTLD_NOW) */
sc[len++] = 0x48; sc[len++] = 0xBE;
uint64_t rtldNow = 2;
memcpy(sc + len, &rtldNow, 8); len += 8;
/* mov rax, dlopen */
sc[len++] = 0x48; sc[len++] = 0xB8;
memcpy(sc + len, &targetDlopen, 8); len += 8;
/* call rax */
sc[len++] = 0xFF; sc[len++] = 0xD0;
/* int3 */
sc[len++] = 0xCC;
writeTargetMem((pid_t)pid, codeAddr, sc, (size_t)len);
/* execute shellcode */
regs = savedRegs;
regs.rip = codeAddr;
regs.rsp = (mmapPage + 4096) & ~0xFULL;
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &regs);
ptrace(PTRACE_CONT, (pid_t)pid, nullptr, nullptr);
waitpid((pid_t)pid, &status, 0);
bool ok = false;
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &regs);
ok = (regs.rax != 0);
}
/* clean up: munmap the page via syscall */
struct user_regs_struct cleanRegs = savedRegs;
cleanRegs.rax = 11; /* __NR_munmap */
cleanRegs.rdi = mmapPage;
cleanRegs.rsi = 4096;
cleanRegs.rip = syscallAddr;
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &cleanRegs);
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
waitpid((pid_t)pid, &status, 0);
/* restore and detach */
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (!ok && errorMsg)
*errorMsg = QStringLiteral("dlopen failed in target.\n"
"Ensure payload is at: %1").arg(path);
return ok;
}
#endif /* _WIN32 / linux injection */
} /* anonymous namespace */
/* ══════════════════════════════════════════════════════════════════════
* RemoteProcessMemoryPlugin
* ══════════════════════════════════════════════════════════════════════ */
RemoteProcessMemoryPlugin::RemoteProcessMemoryPlugin() = default;
RemoteProcessMemoryPlugin::~RemoteProcessMemoryPlugin() = default;
QIcon RemoteProcessMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
}
bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
{
return target.startsWith(QStringLiteral("rpm:"));
}
std::unique_ptr<rcx::Provider>
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
/* target = "rpm:{pid}:{name}" */
QStringList parts = target.split(':');
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
return nullptr;
}
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
QString name = parts.mid(2).join(':'); /* name may contain colons */
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
return nullptr;
}
auto ipc = getOrCreateConnection(pid, errorMsg);
if (!ipc) return nullptr;
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
}
uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
QStringList parts = target.split(':');
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
return 0;
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
if (!ok) return 0;
QMutexLocker lock(&m_connectionsMutex);
auto it = m_connections.constFind(pid);
if (it == m_connections.constEnd() || !(*it)->connected)
return 0;
auto* hdr = static_cast<const RcxRpcHeader*>((*it)->mappedView);
return hdr->imageBase;
}
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
/* ── 1. pick a process ── */
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
QList<ProcessInfo> procs;
for (const auto& pi : pluginProcs) {
ProcessInfo info;
info.pid = pi.pid;
info.name = pi.name;
info.path = pi.path;
info.icon = pi.icon;
procs.append(info);
}
ProcessPicker picker(procs, parent);
if (picker.exec() != QDialog::Accepted) return false;
uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName();
/* ── 2. ask inject or connect ── */
QMessageBox box(parent);
box.setWindowTitle(QStringLiteral("Remote Process Memory"));
box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid));
box.setInformativeText(QStringLiteral("Choose how to connect to the target:"));
QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole);
QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole);
box.addButton(QMessageBox::Cancel);
box.exec();
QAbstractButton* clicked = box.clickedButton();
if (clicked == injectBtn) {
QString injectErr;
if (!injectPayload(pid, &injectErr)) {
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
return false;
}
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
else if (clicked == connectBtn) {
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
return false;
}
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> procs;
#ifdef _WIN32
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return procs;
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
if (Process32FirstW(snap, &entry)) {
do {
PluginProcessInfo info;
info.pid = entry.th32ProcessID;
info.name = QString::fromWCharArray(entry.szExeFile);
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
FALSE, entry.th32ProcessID);
if (hProc) {
wchar_t path[MAX_PATH * 2];
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) {
info.path = QString::fromWCharArray(path);
SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi),
SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)));
#else
info.icon = QIcon(QtWin::fromHICON(sfi.hIcon));
#endif
DestroyIcon(sfi.hIcon);
}
}
CloseHandle(hProc);
}
procs.append(info);
} while (Process32NextW(snap, &entry));
}
CloseHandle(snap);
#else
QDir procDir(QStringLiteral("/proc"));
QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
bool ok;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid));
if (!commFile.open(QIODevice::ReadOnly)) continue;
QString procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
if (procName.isEmpty()) continue;
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue;
QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid));
PluginProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString();
info.icon = defIcon;
procs.append(info);
}
#endif
return procs;
}
std::shared_ptr<IpcClient>
RemoteProcessMemoryPlugin::getOrCreateConnection(
uint32_t pid, QString* errorMsg)
{
QMutexLocker lock(&m_connectionsMutex);
auto it = m_connections.find(pid);
if (it != m_connections.end() && (*it)->connected)
return *it;
auto ipc = std::make_shared<IpcClient>();
if (!ipc->connect(pid)) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
"Is the payload running?").arg(pid);
return nullptr;
}
m_connections[pid] = ipc;
return ipc;
}
/* ── Plugin factory ───────────────────────────────────────────────── */
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new RemoteProcessMemoryPlugin();
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/providers/provider.h"
#include <cstdint>
#include <memory>
#include <QMutex>
#include <QHash>
#include <QVector>
struct IpcClient; /* defined in .cpp */
/* ── Provider ─────────────────────────────────────────────────────── */
class RemoteProcessProvider : public rcx::Provider
{
public:
struct ModuleInfo { QString name; uint64_t base; uint64_t size; };
RemoteProcessProvider(uint32_t pid, const QString& processName,
std::shared_ptr<IpcClient> ipc);
~RemoteProcessProvider() override;
/* required */
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
/* optional */
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_connected; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("RemoteProcess"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& n) const override;
uint32_t pid() const { return m_pid; }
private:
void cacheModules();
uint32_t m_pid;
QString m_processName;
bool m_connected;
uint64_t m_base;
int m_pointerSize = 8;
mutable std::shared_ptr<IpcClient> m_ipc;
QVector<ModuleInfo> m_modules;
};
/* ── Plugin ───────────────────────────────────────────────────────── */
class RemoteProcessMemoryPlugin : public IProviderPlugin
{
public:
RemoteProcessMemoryPlugin();
~RemoteProcessMemoryPlugin() override;
std::string Name() const override { return "Remote Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override {
return "Read/write memory via injected payload (shared-memory IPC)";
}
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target,
QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
private:
std::shared_ptr<IpcClient> getOrCreateConnection(
uint32_t pid, QString* errorMsg);
mutable QMutex m_connectionsMutex;
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
};
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -0,0 +1,612 @@
/*
* rcx_payload -- injected into target process.
*
* Pure Win32 / POSIX, NO Qt, minimal footprint.
* Creates the main IPC channel (shared memory + events/semaphores)
* using PID-only naming and uses a timer queue for polling.
*/
#include "../rcx_rpc_protocol.h"
#ifdef _WIN32
/* ===================================================================
* WINDOWS implementation
* =================================================================== */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
/* ── globals ──────────────────────────────────────────────────────── */
static HANDLE g_hShm = nullptr;
static void* g_mappedView = nullptr;
static HANDLE g_hReqEvent = nullptr;
static HANDLE g_hRspEvent = nullptr;
static HANDLE g_hTimerQueue = nullptr;
static HANDLE g_hPollTimer = nullptr;
static volatile LONG g_initialized = 0;
/* ── memory safety via VirtualQuery ────────────────────────────────── */
inline bool IsReadableProtect(DWORD p)
{
if (p & (PAGE_NOACCESS | PAGE_GUARD))
return false;
const DWORD readable =
PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
return (p & readable) != 0;
}
inline bool IsWritableProtect(DWORD p)
{
if (p & (PAGE_NOACCESS | PAGE_GUARD))
return false;
const DWORD writable =
PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
return (p & writable) != 0;
}
/* Check that the full range [addr, addr+len) is covered by readable pages. */
static bool IsRangeReadable(uintptr_t addr, uint32_t len)
{
uintptr_t end = addr + len;
uintptr_t cur = addr;
while (cur < end) {
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
return false;
if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect))
return false;
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
cur = regionEnd;
}
return true;
}
static bool IsRangeWritable(uintptr_t addr, uint32_t len)
{
uintptr_t end = addr + len;
uintptr_t cur = addr;
while (cur < end) {
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
return false;
if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect))
return false;
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
cur = regionEnd;
}
return true;
}
/* ── command handlers ─────────────────────────────────────────────── */
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
{
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
uint8_t* dest = data + entries[i].dataOffset;
uintptr_t src = static_cast<uintptr_t>(entries[i].address);
if (IsRangeReadable(src, entries[i].length)) {
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
} else {
memset(dest, 0, entries[i].length);
hdr->status = RCX_RPC_STATUS_PARTIAL;
}
/* SEH fallback (commented out, kept for reference):
__try {
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
} __except (EXCEPTION_EXECUTE_HANDLER) {
memset(dest, 0, entries[i].length);
hdr->status = RCX_RPC_STATUS_PARTIAL;
}
*/
}
hdr->responseCount = hdr->requestCount;
}
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
{
uintptr_t dst = static_cast<uintptr_t>(hdr->writeAddress);
if (IsRangeWritable(dst, hdr->writeLength)) {
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
} else {
hdr->status = RCX_RPC_STATUS_ERROR;
}
/* SEH fallback (commented out, kept for reference):
__try {
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
} __except (EXCEPTION_EXECUTE_HANDLER) {
hdr->status = RCX_RPC_STATUS_ERROR;
}
*/
}
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
{
HANDLE hProc = GetCurrentProcess();
HMODULE mods[1024];
DWORD needed = 0;
if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) {
hdr->status = RCX_RPC_STATUS_ERROR;
hdr->responseCount = 0;
return;
}
int count = (int)(needed / sizeof(HMODULE));
if (count > 1024) count = 1024;
uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry));
uint32_t nameDataOff = entryBytes;
for (int i = 0; i < count; ++i) {
MODULEINFO mi{};
WCHAR modName[MAX_PATH];
GetModuleInformation(hProc, mods[i], &mi, sizeof(mi));
int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH);
uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR));
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(data + i * sizeof(RcxRpcModuleEntry));
entry->base = reinterpret_cast<uint64_t>(mi.lpBaseOfDll);
entry->size = static_cast<uint64_t>(mi.SizeOfImage);
entry->nameOffset = nameDataOff;
entry->nameLength = nameBytes;
if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) {
memcpy(data + nameDataOff, modName, nameBytes);
nameDataOff += nameBytes;
}
}
hdr->responseCount = (uint32_t)count;
hdr->totalDataUsed = nameDataOff;
hdr->status = RCX_RPC_STATUS_OK;
}
/* forward declaration */
void RcxPayloadCleanup();
/* ── timer callback (non-blocking poll) ───────────────────────────── */
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
{
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
return;
/* non-blocking check: is there a pending request? */
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
if (rc != WAIT_OBJECT_0)
return;
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
RcxPayloadCleanup();
return;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
SetEvent(g_hRspEvent);
}
/* ── cleanup ──────────────────────────────────────────────────────── */
void RcxPayloadCleanup()
{
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
return;
/* stop the poll timer first */
if (g_hTimerQueue) {
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
g_hTimerQueue = nullptr;
g_hPollTimer = nullptr;
}
/* mark not-ready */
if (g_mappedView) {
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
}
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
InterlockedExchange(&g_initialized, 0);
}
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
extern "C" __declspec(dllexport)
bool RcxPayloadInit()
{
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
return true; /* already initialized */
uint32_t pid = GetCurrentProcessId();
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) {
InterlockedExchange(&g_initialized, 0);
return false;
}
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) {
CloseHandle(g_hShm); g_hShm = nullptr;
InterlockedExchange(&g_initialized, 0);
return false;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from PEB */
{
uint64_t peb;
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
}
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) {
RcxPayloadCleanup();
return false;
}
/* create dedicated timer queue + fast poll timer (10ms interval) */
g_hTimerQueue = CreateTimerQueue();
if (!g_hTimerQueue) {
RcxPayloadCleanup();
return false;
}
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
RcxPollTimerCallback, nullptr,
0, /* start immediately */
10, /* 10ms repeat */
WT_EXECUTEDEFAULT)) {
RcxPayloadCleanup();
return false;
}
/* mark ready */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
return true;
}
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_DETACH) {
RcxPayloadCleanup();
}
return TRUE;
}
#else
/* ===================================================================
* LINUX implementation
* =================================================================== */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
/* ── globals ──────────────────────────────────────────────────────── */
static int g_shmFd = -1;
static void* g_mappedView = nullptr;
static sem_t* g_reqSem = SEM_FAILED;
static sem_t* g_rspSem = SEM_FAILED;
static pthread_t g_thread;
static volatile int g_shutdown = 0;
static volatile int g_threadRunning = 0;
static int g_memFd = -1; /* /proc/self/mem for safe access */
static char g_shmName[128];
static char g_reqName[128];
static char g_rspName[128];
/* ── safe memory access via /proc/self/mem ────────────────────────── */
static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status)
{
ssize_t n = pread(g_memFd, dest, len, (off_t)addr);
if (n < (ssize_t)len) {
if (n > 0)
memset((uint8_t*)dest + n, 0, len - (uint32_t)n);
else
memset(dest, 0, len);
*status = RCX_RPC_STATUS_PARTIAL;
}
}
static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status)
{
ssize_t n = pwrite(g_memFd, src, len, (off_t)addr);
if (n < (ssize_t)len)
*status = RCX_RPC_STATUS_ERROR;
}
/* ── command handlers ─────────────────────────────────────────────── */
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
{
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
uint8_t* dest = data + entries[i].dataOffset;
safe_read(entries[i].address, dest, entries[i].length, &hdr->status);
}
hdr->responseCount = hdr->requestCount;
}
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
{
safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status);
}
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
{
FILE* f = fopen("/proc/self/maps", "r");
if (!f) {
hdr->status = RCX_RPC_STATUS_ERROR;
hdr->responseCount = 0;
return;
}
/* first pass: collect unique file-backed mappings */
struct ModRange { uint64_t base; uint64_t end; char path[512]; };
static ModRange modules[512]; /* static to avoid large stack alloc */
int modCount = 0;
char line[1024];
while (fgets(line, sizeof(line), f) && modCount < 512) {
uint64_t start, end;
char perms[8] = {}, path[512] = {};
if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]",
&start, &end, perms, path) < 4)
continue;
/* skip non-file / special mappings */
/* trim leading whitespace from path */
char* p = path;
while (*p == ' ' || *p == '\t') ++p;
if (*p != '/') continue;
if (strncmp(p, "/dev/", 5) == 0) continue;
if (strncmp(p, "/memfd:", 7) == 0) continue;
bool found = false;
for (int i = 0; i < modCount; ++i) {
if (strcmp(modules[i].path, p) == 0) {
if (start < modules[i].base) modules[i].base = start;
if (end > modules[i].end) modules[i].end = end;
found = true;
break;
}
}
if (!found) {
modules[modCount].base = start;
modules[modCount].end = end;
strncpy(modules[modCount].path, p, 511);
modules[modCount].path[511] = '\0';
++modCount;
}
}
fclose(f);
/* write entries + name strings into data region */
uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry));
uint32_t nameDataOff = entryBytes;
for (int i = 0; i < modCount; ++i) {
const char* basename = strrchr(modules[i].path, '/');
basename = basename ? basename + 1 : modules[i].path;
uint32_t nameLen = (uint32_t)strlen(basename);
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(
data + (uint32_t)i * sizeof(RcxRpcModuleEntry));
entry->base = modules[i].base;
entry->size = modules[i].end - modules[i].base;
entry->nameOffset = nameDataOff;
entry->nameLength = nameLen;
if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) {
memcpy(data + nameDataOff, basename, nameLen);
nameDataOff += nameLen;
}
}
hdr->responseCount = (uint32_t)modCount;
hdr->totalDataUsed = nameDataOff;
hdr->status = RCX_RPC_STATUS_OK;
}
/* ── server thread ────────────────────────────────────────────────── */
static void* server_thread_func(void*)
{
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
__atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE);
while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) {
/* timed wait: 250ms */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 250000000;
if (ts.tv_nsec >= 1000000000) {
ts.tv_sec += 1;
ts.tv_nsec -= 1000000000;
}
int rc = sem_timedwait(g_reqSem, &ts);
if (rc != 0) {
if (errno == ETIMEDOUT) continue;
break;
}
hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
break;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
sem_post(g_rspSem);
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
break;
}
__atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE);
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
return nullptr;
}
/* ── init / cleanup ───────────────────────────────────────────────── */
static void payload_cleanup()
{
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
/* wake the thread if blocked */
if (g_reqSem != SEM_FAILED) sem_post(g_reqSem);
if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 2;
pthread_timedjoin_np(g_thread, nullptr, &ts);
}
if (g_mappedView && g_mappedView != MAP_FAILED) {
munmap(g_mappedView, RCX_RPC_SHM_SIZE);
g_mappedView = nullptr;
}
if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; }
if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; }
if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; }
/* unlink named objects */
if (g_shmName[0]) shm_unlink(g_shmName);
if (g_reqName[0]) sem_unlink(g_reqName);
if (g_rspName[0]) sem_unlink(g_rspName);
if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; }
}
__attribute__((constructor))
static void payload_init()
{
uint32_t pid = (uint32_t)getpid();
/* ── open /proc/self/mem for safe access ── */
g_memFd = open("/proc/self/mem", O_RDWR);
if (g_memFd < 0) return;
/* ── create main shared memory (PID-only naming) ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
if (g_shmFd < 0) return;
if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) {
close(g_shmFd); g_shmFd = -1; return;
}
g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, g_shmFd, 0);
if (g_mappedView == MAP_FAILED) {
g_mappedView = nullptr;
close(g_shmFd); g_shmFd = -1;
return;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from /proc/self/maps: first executable mapping */
{
FILE* f = fopen("/proc/self/maps", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f)) {
uint64_t start;
char perms[8] = {};
if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') {
hdr->imageBase = start;
break;
}
}
fclose(f);
}
}
/* ── create semaphores ── */
g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0);
g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0);
if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) {
payload_cleanup();
return;
}
/* ── start server thread (it will set payloadReady = 1) ── */
__atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE);
if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) {
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
payload_cleanup();
return;
}
pthread_detach(g_thread);
}
__attribute__((destructor))
static void payload_deinit()
{
payload_cleanup();
}
#endif /* _WIN32 / linux */

View File

@@ -0,0 +1,115 @@
/*
* RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO.
* No dependencies beyond standard C headers.
*/
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ── constants ─────────────────────────────────────────────────────── */
#define RCX_RPC_VERSION 1
#define RCX_RPC_MAX_BATCH 256
#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */
#define RCX_RPC_HEADER_SIZE 4096
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
/* status codes */
#define RCX_RPC_STATUS_OK 0
#define RCX_RPC_STATUS_ERROR 1
#define RCX_RPC_STATUS_PARTIAL 2
/* ── commands ──────────────────────────────────────────────────────── */
#ifdef __cplusplus
enum RcxRpcCommand : uint32_t {
#else
typedef uint32_t RcxRpcCommand;
enum {
#endif
RPC_CMD_NONE = 0,
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
RPC_CMD_WRITE = 2, /* single write */
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
RPC_CMD_PING = 4, /* heartbeat */
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
};
/* ── wire structs (natural alignment, verified by static_assert) ─── */
struct RcxRpcReadEntry {
uint64_t address;
uint32_t length;
uint32_t dataOffset; /* offset into data region for response bytes */
};
struct RcxRpcModuleEntry {
uint64_t base;
uint64_t size;
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
uint32_t nameLength; /* in bytes */
};
/*
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
*
* offset field
* ------ -----
* 0 version (4)
* 4 payloadReady (4)
* 8 command (4)
* 12 requestCount (4)
* 16 writeAddress (8)
* 24 writeLength (4)
* 28 status (4)
* 32 responseCount (4)
* 36 totalDataUsed (4)
* 40 imageBase (8) -- main module base from PEB / procfs
* 48 pointerSize (4) -- 4 for 32-bit, 8 for 64-bit payload
* 52 _pad[4044]
*/
struct RcxRpcHeader {
uint32_t version;
uint32_t payloadReady; /* payload sets to 1 after init */
uint32_t command; /* RcxRpcCommand */
uint32_t requestCount;
uint64_t writeAddress;
uint32_t writeLength;
uint32_t status; /* RCX_RPC_STATUS_* */
uint32_t responseCount;
uint32_t totalDataUsed;
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
uint32_t pointerSize; /* 4 for 32-bit, 8 for 64-bit payload */
uint8_t _pad[RCX_RPC_HEADER_SIZE - 52];
};
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
#else
snprintf(buf, n, "/rcx_shm_%u", pid);
#endif
}
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
#else
snprintf(buf, n, "/rcx_req_%u", pid);
#endif
}
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
#else
snprintf(buf, n, "/rcx_rsp_%u", pid);
#endif
}
#ifdef __cplusplus
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
#endif

View File

@@ -0,0 +1,593 @@
/*
* test_rpc_client -- connects to a running test_rpc_host (or spawns one),
* exercises every RPC command, and benchmarks throughput.
*
* Usage:
* test_rpc_client (auto-spawn host)
* test_rpc_client <pid> [testbuf_hex testlen]
*/
#include "../rcx_rpc_protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <chrono>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <unistd.h>
# include <fcntl.h>
# include <sys/mman.h>
# include <semaphore.h>
# include <libgen.h>
# include <limits.h>
#endif
/* ══════════════════════════════════════════════════════════════════════
* Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient)
* ══════════════════════════════════════════════════════════════════════ */
struct TestIpcClient {
#ifdef _WIN32
HANDLE hShm = nullptr;
HANDLE hReqEvent = nullptr;
HANDLE hRspEvent = nullptr;
#else
int shmFd = -1;
sem_t* reqSem = SEM_FAILED;
sem_t* rspSem = SEM_FAILED;
#endif
void* view = nullptr;
bool ok = false;
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
if (GetTickCount64() >= deadline) return false;
Sleep(10);
}
view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!view) { CloseHandle(hShm); hShm = nullptr; return false; }
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
if (!hReqEvent || !hRspEvent) return false;
#else
auto start = std::chrono::steady_clock::now();
while (true) {
shmFd = shm_open(shmName, O_RDWR, 0);
if (shmFd >= 0) break;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(10000);
}
view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shmFd, 0);
if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; }
reqSem = sem_open(reqName, 0);
rspSem = sem_open(rspName, 0);
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false;
#endif
/* wait for payloadReady */
auto* hdr = (RcxRpcHeader*)view;
#ifdef _WIN32
while (!hdr->payloadReady) {
if (GetTickCount64() >= deadline) return false;
Sleep(5);
}
#else
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(5000);
}
#endif
ok = true;
return true;
}
void disconnect()
{
#ifdef _WIN32
if (view) { UnmapViewOfFile(view); view = nullptr; }
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
#else
if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; }
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
#endif
ok = false;
}
bool signalAndWait(int timeoutMs = 2000)
{
#ifdef _WIN32
SetEvent(hReqEvent);
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
#else
sem_post(reqSem);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeoutMs / 1000;
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
return sem_timedwait(rspSem, &ts) == 0;
#endif
}
/* ── RPC helpers ──────────────────────────────────────────────── */
bool rpc_ping()
{
auto* hdr = (RcxRpcHeader*)view;
hdr->command = RPC_CMD_PING;
hdr->status = RCX_RPC_STATUS_OK;
return signalAndWait();
}
bool rpc_read(uint64_t addr, void* buf, uint32_t len)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = 1;
hdr->status = RCX_RPC_STATUS_OK;
auto* entry = (RcxRpcReadEntry*)data;
entry->address = addr;
entry->length = len;
entry->dataOffset = sizeof(RcxRpcReadEntry);
if (!signalAndWait()) return false;
memcpy(buf, data + entry->dataOffset, len);
return true;
}
bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens,
uint32_t count, uint8_t* outBuf)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = count;
hdr->status = RCX_RPC_STATUS_OK;
/* lay out entries, then data offsets after all entries */
uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry);
uint32_t dataOff = entriesSize;
for (uint32_t i = 0; i < count; ++i) {
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
e->address = addrs[i];
e->length = lens[i];
e->dataOffset = dataOff;
dataOff += lens[i];
}
if (!signalAndWait()) return false;
/* copy out response data */
uint32_t off = 0;
for (uint32_t i = 0; i < count; ++i) {
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
memcpy(outBuf + off, data + e->dataOffset, e->length);
off += e->length;
}
return true;
}
bool rpc_write(uint64_t addr, const void* buf, uint32_t len)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_WRITE;
hdr->writeAddress = addr;
hdr->writeLength = len;
hdr->status = RCX_RPC_STATUS_OK;
memcpy(data, buf, len);
if (!signalAndWait()) return false;
return hdr->status == RCX_RPC_STATUS_OK;
}
struct ModInfo { uint64_t base; uint64_t size; char name[256]; };
int rpc_enum_modules(ModInfo* out, int maxOut)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_ENUM_MODULES;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) return -1;
if (hdr->status != RCX_RPC_STATUS_OK) return -1;
int count = (int)hdr->responseCount;
if (count > maxOut) count = maxOut;
for (int i = 0; i < count; ++i) {
auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry));
out[i].base = entry->base;
out[i].size = entry->size;
#ifdef _WIN32
/* names are UTF-16 on Windows */
int wchars = (int)(entry->nameLength / sizeof(wchar_t));
WideCharToMultiByte(CP_UTF8, 0,
(const wchar_t*)(data + entry->nameOffset), wchars,
out[i].name, 255, nullptr, nullptr);
out[i].name[255] = '\0';
#else
int nLen = (int)entry->nameLength;
if (nLen > 255) nLen = 255;
memcpy(out[i].name, data + entry->nameOffset, nLen);
out[i].name[nLen] = '\0';
#endif
}
return count;
}
void rpc_shutdown()
{
auto* hdr = (RcxRpcHeader*)view;
hdr->command = RPC_CMD_SHUTDOWN;
hdr->status = RCX_RPC_STATUS_OK;
signalAndWait(500);
}
};
/* ══════════════════════════════════════════════════════════════════════
* Auto-spawn host
* ══════════════════════════════════════════════════════════════════════ */
#ifdef _WIN32
static HANDLE g_hostProcess = nullptr;
#else
static pid_t g_hostPid = 0;
#endif
static FILE* g_hostPipe = nullptr;
static bool spawn_host(uint32_t* outPid,
uint64_t* outTestBuf, uint32_t* outTestLen)
{
/* resolve path to test_rpc_host next to ourselves */
char cmd[2048];
#ifdef _WIN32
char exePath[MAX_PATH];
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
char* slash = strrchr(exePath, '\\');
if (!slash) slash = strrchr(exePath, '/');
if (slash) *(slash + 1) = '\0';
snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath);
g_hostPipe = _popen(cmd, "r");
#else
char exePath[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
if (n <= 0) return false;
exePath[n] = '\0';
char* dir = dirname(exePath);
snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir);
g_hostPipe = popen(cmd, "r");
#endif
if (!g_hostPipe) {
fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd);
return false;
}
/* read READY line */
char line[512];
if (!fgets(line, sizeof(line), g_hostPipe)) {
fprintf(stderr, "ERROR: no output from host\n");
return false;
}
/* parse: READY pid=X testbuf=0xZ testlen=N */
unsigned long long tbuf = 0;
unsigned tlen = 0;
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
outPid, &tbuf, &tlen) < 1) {
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
return false;
}
*outTestBuf = (uint64_t)tbuf;
*outTestLen = (uint32_t)tlen;
return true;
}
static void cleanup_host()
{
if (g_hostPipe) {
#ifdef _WIN32
_pclose(g_hostPipe);
#else
pclose(g_hostPipe);
#endif
g_hostPipe = nullptr;
}
}
/* ══════════════════════════════════════════════════════════════════════
* Printing helpers
* ══════════════════════════════════════════════════════════════════════ */
static void print_pass(const char* name) { printf(" [PASS] %s\n", name); }
static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); }
/* ══════════════════════════════════════════════════════════════════════
* main
* ══════════════════════════════════════════════════════════════════════ */
int main(int argc, char** argv)
{
uint32_t pid = 0;
uint64_t testBuf = 0;
uint32_t testLen = 0;
bool autoMode = false;
if (argc >= 2) {
pid = (uint32_t)atoi(argv[1]);
if (argc >= 4) {
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
testLen = (uint32_t)atoi(argv[3]);
}
} else {
autoMode = true;
printf("Auto-spawning test_rpc_host...\n");
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
}
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
pid, (unsigned long long)testBuf, testLen);
/* ── connect ── */
TestIpcClient ipc;
if (!ipc.connect(pid)) {
fprintf(stderr, "ERROR: IPC connect failed\n");
if (autoMode) cleanup_host();
return 1;
}
printf("=== Functional Tests ===\n");
/* ── test: ping ── */
if (ipc.rpc_ping()) print_pass("Ping");
else print_fail("Ping");
/* ── test: enumerate modules ── */
TestIpcClient::ModInfo mods[512];
int modCount = ipc.rpc_enum_modules(mods, 512);
if (modCount > 0) {
printf(" [PASS] EnumModules (%d modules)\n", modCount);
printf(" first: %s base=0x%llx size=0x%llx\n",
mods[0].name,
(unsigned long long)mods[0].base,
(unsigned long long)mods[0].size);
} else {
print_fail("EnumModules");
}
/* ── test: read module header (MZ / ELF magic) ── */
if (modCount > 0) {
uint8_t header[4] = {};
if (ipc.rpc_read(mods[0].base, header, 4)) {
#ifdef _WIN32
if (header[0] == 'M' && header[1] == 'Z')
print_pass("ReadModuleHeader (MZ)");
else
print_fail("ReadModuleHeader (expected MZ)");
#else
if (header[0] == 0x7F && header[1] == 'E' &&
header[2] == 'L' && header[3] == 'F')
print_pass("ReadModuleHeader (ELF)");
else
print_fail("ReadModuleHeader (expected ELF)");
#endif
} else {
print_fail("ReadModuleHeader (read failed)");
}
}
/* ── test: read test buffer (known pattern) ── */
if (testBuf && testLen >= 4096) {
uint8_t buf[4096];
if (ipc.rpc_read(testBuf, buf, 4096)) {
bool good = true;
for (int i = 0; i < 4096; ++i) {
if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; }
}
if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)");
else print_fail("ReadTestBuffer (pattern mismatch)");
} else {
print_fail("ReadTestBuffer (read failed)");
}
}
/* ── test: write ── */
if (testBuf && testLen >= 16) {
uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF};
if (ipc.rpc_write(testBuf, patch, 4)) {
uint8_t verify[4] = {};
ipc.rpc_read(testBuf, verify, 4);
if (memcmp(verify, patch, 4) == 0)
print_pass("Write + ReadBack (0xDEADBEEF)");
else
print_fail("Write + ReadBack (readback mismatch)");
} else {
print_fail("Write (write failed)");
}
}
/* ── test: batch read ── */
if (testBuf && testLen >= 8192) {
const uint32_t N = 4;
uint64_t addrs[N];
uint32_t lens[N];
for (uint32_t i = 0; i < N; ++i) {
addrs[i] = testBuf + i * 1024;
lens[i] = 1024;
}
uint8_t out[4096];
if (ipc.rpc_read_batch(addrs, lens, N, out)) {
print_pass("BatchRead (4 x 1024 bytes)");
} else {
print_fail("BatchRead");
}
}
printf("\n=== Benchmarks ===\n");
/* choose a valid address for benchmarking */
uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0);
if (!benchAddr) {
printf(" (no valid address for benchmarks, skipping)\n");
} else {
/* ── benchmark: single 4 KB reads ── */
{
const int ITERS = 10000;
const int PAGE = 4096;
uint8_t tmp[4096];
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read(benchAddr, tmp, PAGE);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
printf(" Single 4 KB reads:\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/read\n", us / ITERS);
}
/* ── benchmark: single 64 B reads (pointer-chase-size) ── */
{
const int ITERS = 50000;
const int SZ = 64;
uint8_t tmp[64];
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read(benchAddr, tmp, SZ);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalKB = (double)ITERS * SZ / 1024.0;
printf(" Single 64 B reads (pointer-chase):\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f KB\n", totalKB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f KB/s\n", totalKB / secs);
printf(" Avg latency: %.2f us/read\n", us / ITERS);
}
/* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */
{
const int ITERS = 2000;
const uint32_t BATCH = 50;
const uint32_t PAGE = 4096;
uint64_t addrs[BATCH];
uint32_t lens[BATCH];
for (uint32_t i = 0; i < BATCH; ++i) {
/* wrap within test buffer or module */
addrs[i] = benchAddr + (i * PAGE) % 65536;
lens[i] = PAGE;
}
/* allocate response buffer */
uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE);
if (!outBuf) {
printf(" (batch malloc failed, skipping)\n");
} else {
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read_batch(addrs, lens, BATCH, outBuf);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0);
printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE);
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/batch\n", us / ITERS);
printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH));
free(outBuf);
}
}
/* ── benchmark: write 4 KB ── */
if (testBuf && testLen >= 4096) {
const int ITERS = 10000;
const int PAGE = 4096;
uint8_t tmp[4096];
memset(tmp, 0x42, sizeof(tmp));
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_write(testBuf, tmp, PAGE);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
printf(" Write 4 KB:\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/write\n", us / ITERS);
}
}
/* ── shutdown ── */
printf("\nSending shutdown...\n");
ipc.rpc_shutdown();
ipc.disconnect();
if (autoMode) {
/* wait for host to exit */
#ifdef _WIN32
Sleep(500);
#else
usleep(500000);
#endif
cleanup_host();
}
printf("Done.\n");
return 0;
}

View File

@@ -0,0 +1,187 @@
/*
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
*
* Usage: test_rpc_host
*
* Prints a READY line (machine-parseable), then waits for the payload
* to shut down (RPC_CMD_SHUTDOWN from the client).
*/
#include "../rcx_rpc_protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <unistd.h>
# include <dlfcn.h>
# include <fcntl.h>
# include <sys/mman.h>
# include <semaphore.h>
# include <libgen.h>
# include <limits.h>
#endif
/* ── Helpers ──────────────────────────────────────────────────────── */
static uint32_t current_pid()
{
#ifdef _WIN32
return (uint32_t)GetCurrentProcessId();
#else
return (uint32_t)getpid();
#endif
}
static void sleep_ms(int ms)
{
#ifdef _WIN32
Sleep((DWORD)ms);
#else
usleep((useconds_t)ms * 1000);
#endif
}
/* Resolve payload path relative to this executable */
static int payload_path(char* out, int outLen)
{
#ifdef _WIN32
char exePath[MAX_PATH];
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
char* slash = strrchr(exePath, '\\');
if (!slash) slash = strrchr(exePath, '/');
if (slash) *(slash + 1) = '\0';
snprintf(out, outLen, "%srcx_payload.dll", exePath);
#else
char exePath[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
if (n <= 0) return -1;
exePath[n] = '\0';
char* dir = dirname(exePath);
snprintf(out, outLen, "%s/rcx_payload.so", dir);
#endif
return 0;
}
/* Open the main shared memory (read-only, just to monitor payloadReady) */
static void* open_main_shm(uint32_t pid)
{
char shmName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
#ifdef _WIN32
HANDLE h = nullptr;
for (int i = 0; i < 500; ++i) {
h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName);
if (h) break;
sleep_ms(10);
}
if (!h) return nullptr;
void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader));
return v;
#else
int fd = -1;
for (int i = 0; i < 500; ++i) {
fd = shm_open(shmName, O_RDONLY, 0);
if (fd >= 0) break;
sleep_ms(10);
}
if (fd < 0) return nullptr;
void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0);
close(fd);
return (v == MAP_FAILED) ? nullptr : v;
#endif
}
/* ── Test buffer (known pattern for client to verify reads/writes) ── */
static uint8_t g_testBuf[65536];
/* ── main ─────────────────────────────────────────────────────────── */
int main(int, char**)
{
uint32_t pid = current_pid();
/* fill test buffer with known pattern */
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
g_testBuf[i] = (uint8_t)(i & 0xFF);
/* load payload */
char plPath[1024];
if (payload_path(plPath, sizeof(plPath)) != 0) {
fprintf(stderr, "ERROR: cannot determine payload path\n");
return 1;
}
#ifdef _WIN32
HMODULE hPayload = LoadLibraryA(plPath);
if (!hPayload) {
fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n",
plPath, GetLastError());
return 1;
}
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
typedef bool (*RcxPayloadInitFn)();
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
if (!pfnInit || !pfnInit()) {
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
FreeLibrary(hPayload);
return 1;
}
#else
void* hPayload = dlopen(plPath, RTLD_NOW);
if (!hPayload) {
fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror());
return 1;
}
#endif
/* open main shm and wait for payloadReady */
void* shmView = open_main_shm(pid);
if (!shmView) {
fprintf(stderr, "ERROR: failed to open main shared memory\n");
return 1;
}
RcxRpcHeader* hdr = (RcxRpcHeader*)shmView;
for (int i = 0; i < 500; ++i) {
if (hdr->payloadReady) break;
sleep_ms(10);
}
if (!hdr->payloadReady) {
fprintf(stderr, "ERROR: payload did not become ready\n");
return 1;
}
/* print READY line for the client to parse */
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
pid,
(unsigned long long)(uintptr_t)g_testBuf,
(unsigned)sizeof(g_testBuf));
fflush(stdout);
/* wait until payload shuts down */
while (hdr->payloadReady)
sleep_ms(100);
printf("Payload shut down, exiting.\n");
#ifdef _WIN32
/* give the timer queue a moment to drain */
Sleep(200);
FreeLibrary(hPayload);
if (shmView) UnmapViewOfFile(shmView);
#else
usleep(200000);
dlclose(hPayload);
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
#endif
return 0;
}

View File

@@ -165,6 +165,10 @@ void WinDbgMemoryProvider::initInterfaces()
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_dataSpaces;
hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2);
qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_dataSpaces2;
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_control;
@@ -197,53 +201,28 @@ void WinDbgMemoryProvider::querySessionInfo()
}
}
if (m_symbols) {
ULONG numModules = 0, numUnloaded = 0;
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr
<< "loaded=" << numModules << "unloaded=" << numUnloaded;
if (SUCCEEDED(hr) && numModules > 0) {
char modName[256] = {};
ULONG modSize = 0;
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
modName, sizeof(modName), &modSize,
nullptr, 0, nullptr);
if (SUCCEEDED(hr) && modSize > 0)
m_name = QString::fromUtf8(modName);
// Query effective processor type for pointer size detection
if (m_control) {
ULONG procType = 0;
hr = m_control->GetEffectiveProcessorType(&procType);
if (SUCCEEDED(hr)) {
// IMAGE_FILE_MACHINE_I386 = 0x014C
if (procType == 0x014C)
m_pointerSize = 4;
qDebug() << "[WinDbg] EffectiveProcessorType=" << Qt::hex << procType
<< "pointerSize=" << m_pointerSize;
}
}
if (m_name.isEmpty())
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
if (m_symbols) {
ULONG numModules = 0, numUnloaded = 0;
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
if (SUCCEEDED(hr) && numModules > 0) {
ULONG64 moduleBase = 0;
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
if (SUCCEEDED(hr))
m_base = moduleBase;
}
}
if (m_base && m_dataSpaces) {
uint8_t probe[2] = {};
ULONG got = 0;
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
<< "hr=" << (unsigned long)hr << "got=" << got
<< "bytes:" << (int)probe[0] << (int)probe[1];
if (FAILED(hr) || got == 0) {
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
cleanup();
return;
}
}
// WinDbg provides access to the entire virtual address space.
// Do NOT auto-select a module as base — let the user set their
// own base address. m_base stays 0 so the controller won't
// override tree.baseAddress.
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
: QStringLiteral("WinDbg (Dump)");
qDebug() << "[WinDbg] Ready. name=" << m_name
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
<< "isLive=" << m_isLive;
#endif
}
@@ -289,10 +268,11 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider()
void WinDbgMemoryProvider::cleanup()
{
#ifdef _WIN32
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
if (m_control) { m_control->Release(); m_control = nullptr; }
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
if (m_client) { m_client->Release(); m_client = nullptr; }
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
if (m_control) { m_control->Release(); m_control = nullptr; }
if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; }
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
if (m_client) { m_client->Release(); m_client = nullptr; }
#endif
}
@@ -304,9 +284,19 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
bool result = false;
dispatchToOwner([&]() {
ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead);
if (FAILED(hr) || (int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead);
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
result = true;
return;
}
// Partial or failed read — zero-fill remainder and log
memset((char*)buf + bytesRead, 0, len - bytesRead);
++m_readFailCount;
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
<< "len=" << Qt::dec << len
<< "hr=0x" << Qt::hex << (unsigned long)hr
<< "got=" << Qt::dec << bytesRead;
result = bytesRead > 0;
});
return result;
@@ -324,7 +314,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
bool result = false;
dispatchToOwner([&]() {
ULONG bytesWritten = 0;
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
(ULONG)len, &bytesWritten);
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
});
@@ -364,7 +354,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
char nameBuf[512] = {};
ULONG nameSize = 0;
ULONG64 displacement = 0;
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
&nameSize, &displacement);
if (SUCCEEDED(hr) && nameSize > 0) {
result = QString::fromUtf8(nameBuf);
@@ -379,6 +369,112 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
#endif
}
QVector<rcx::MemoryRegion> WinDbgMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
#ifdef _WIN32
if (!m_dataSpaces) return regions;
// Enumerate modules — used for tagging (user-mode) or as the primary
// source of regions (kernel-mode, where QueryVirtual is unavailable).
struct ModInfo { uint64_t base; uint64_t size; QString name; };
QVector<ModInfo> modules;
if (m_symbols) {
dispatchToOwner([&]() {
ULONG loaded = 0, unloaded = 0;
if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded)))
return;
for (ULONG i = 0; i < loaded; i++) {
ULONG64 modBase = 0;
if (FAILED(m_symbols->GetModuleByIndex(i, &modBase)))
continue;
DEBUG_MODULE_PARAMETERS params = {};
if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, &params)))
continue;
char nameBuf[256] = {};
ULONG nameSize = 0;
m_symbols->GetModuleNames(i, 0,
nullptr, 0, nullptr,
nameBuf, sizeof(nameBuf), &nameSize,
nullptr, 0, nullptr);
ModInfo mi;
mi.base = modBase;
mi.size = params.Size;
mi.name = QString::fromUtf8(nameBuf);
modules.append(mi);
}
});
}
// Try QueryVirtual first (user-mode debugging / user-mode dumps).
// MSDN: "This method is not available in kernel-mode debugging."
if (m_dataSpaces2) {
dispatchToOwner([&]() {
ULONG64 addr = 0;
int safety = 0;
constexpr int kMaxRegions = 500000;
while (safety++ < kMaxRegions) {
MEMORY_BASIC_INFORMATION64 mbi = {};
HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi);
if (FAILED(hr))
break;
if (mbi.State == MEM_COMMIT &&
!(mbi.Protect & PAGE_NOACCESS) &&
!(mbi.Protect & PAGE_GUARD))
{
rcx::MemoryRegion region;
region.base = mbi.BaseAddress;
region.size = mbi.RegionSize;
region.readable = true;
region.writable = (mbi.Protect & PAGE_READWRITE) ||
(mbi.Protect & PAGE_WRITECOPY) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
(mbi.Protect & PAGE_EXECUTE_READ) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
for (const auto& mod : modules) {
if (region.base >= mod.base && region.base < mod.base + mod.size) {
region.moduleName = mod.name;
break;
}
}
regions.append(region);
}
ULONG64 next = mbi.BaseAddress + mbi.RegionSize;
if (next <= addr) break;
addr = next;
}
});
}
// Fallback for kernel-mode debugging: QueryVirtual is unavailable,
// so use loaded modules as scannable regions. Each module image
// becomes one region — the scanner reads through module code/data.
if (regions.isEmpty() && !modules.isEmpty()) {
for (const auto& mod : modules) {
if (mod.size == 0) continue;
rcx::MemoryRegion region;
region.base = mod.base;
region.size = mod.size;
region.readable = true;
region.writable = false;
region.executable = true;
region.moduleName = mod.name;
regions.append(region);
}
}
#endif
return regions;
}
// ──────────────────────────────────────────────────────────────────────────
// WinDbgMemoryPlugin implementation
// ──────────────────────────────────────────────────────────────────────────
@@ -436,7 +532,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
QDialog dlg(parent);
dlg.setWindowTitle("WinDbg Settings");
dlg.resize(460, 260);
dlg.resize(460, 300);
QPalette dlgPal = qApp->palette();
dlg.setPalette(dlgPal);
@@ -446,7 +542,9 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
layout->addWidget(new QLabel(
"Connect to a running WinDbg debug server.\n"
"In WinDbg, run: .server tcp:port=5055"));
"In WinDbg, run: .server tcp:port=5055\n\n"
"Non-invasive debug and dump files only.\n"
"Execution control (bp, g, t, p) is not supported."));
layout->addSpacing(8);
layout->addWidget(new QLabel("Connection string:"));

View File

@@ -9,6 +9,7 @@
// Forward declarations for DbgEng COM interfaces
struct IDebugClient;
struct IDebugDataSpaces;
struct IDebugDataSpaces2;
struct IDebugControl;
struct IDebugSymbols;
@@ -59,10 +60,11 @@ public:
QString name() const override { return m_name; }
QString kind() const override { return QStringLiteral("WinDbg"); }
QString getSymbol(uint64_t addr) const override;
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
int pointerSize() const override { return m_pointerSize; }
private:
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
@@ -74,16 +76,19 @@ private:
template<typename Fn>
void dispatchToOwner(Fn&& fn) const;
IDebugClient* m_client = nullptr;
IDebugDataSpaces* m_dataSpaces = nullptr;
IDebugControl* m_control = nullptr;
IDebugSymbols* m_symbols = nullptr;
IDebugClient* m_client = nullptr;
IDebugDataSpaces* m_dataSpaces = nullptr;
IDebugDataSpaces2* m_dataSpaces2 = nullptr;
IDebugControl* m_control = nullptr;
IDebugSymbols* m_symbols = nullptr;
QString m_name;
uint64_t m_base = 0;
bool m_isLive = false;
bool m_writable = false;
int m_pointerSize = 8;
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
mutable int m_readFailCount = 0;
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
// transport is thread-affine — all calls must happen on the thread

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

168
scripts/build_macos.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
print_help() {
cat <<'EOF'
Reclass macOS Build Script
Usage:
./scripts/build_macos.sh [options]
Options:
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
--build-dir <path> Build directory (default: <repo>/build)
--generator <name> CMake generator (default: Ninja if available)
--clean Remove build directory before configuring
--rebuild Clean then build
--package Run macdeployqt and create a zip
--tests Run ctest after build
-h, --help Show this help
Notes:
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
- If Qt is installed via Homebrew, the script will try to detect it.
EOF
}
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
project_root="$(cd "${script_dir}/.." && pwd)"
qt_dir=""
build_type="Release"
build_dir="${project_root}/build"
generator=""
do_clean="false"
do_package="false"
do_tests="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--qt-dir)
qt_dir="${2:-}"
shift 2
;;
--build-type)
build_type="${2:-}"
shift 2
;;
--build-dir)
build_dir="${2:-}"
shift 2
;;
--generator)
generator="${2:-}"
shift 2
;;
--clean)
do_clean="true"
shift
;;
--rebuild)
do_clean="true"
shift
;;
--package)
do_package="true"
shift
;;
--tests)
do_tests="true"
shift
;;
-h|--help)
print_help
exit 0
;;
*)
echo "Unknown argument: $1" >&2
print_help
exit 1
;;
esac
done
if [[ -z "${qt_dir}" ]]; then
if [[ -n "${QTDIR:-}" ]]; then
qt_dir="${QTDIR}"
elif [[ -n "${Qt6_DIR:-}" ]]; then
qt_dir="${Qt6_DIR}"
elif command -v brew >/dev/null 2>&1; then
if brew --prefix qt >/dev/null 2>&1; then
qt_dir="$(brew --prefix qt)"
fi
fi
fi
if ! command -v cmake >/dev/null 2>&1; then
echo "ERROR: cmake not found. Install CMake and try again." >&2
exit 1
fi
if [[ -z "${generator}" ]]; then
if command -v ninja >/dev/null 2>&1; then
generator="Ninja"
fi
fi
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
echo "Cleaning build directory: ${build_dir}"
rm -rf "${build_dir}"
fi
mkdir -p "${build_dir}"
cmake_args=(
-S "${project_root}"
-B "${build_dir}"
-DCMAKE_BUILD_TYPE="${build_type}"
)
if [[ -n "${generator}" ]]; then
cmake_args+=(-G "${generator}")
fi
if [[ -n "${qt_dir}" ]]; then
export PATH="${qt_dir}/bin:${PATH}"
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
fi
echo "Configuring..."
cmake "${cmake_args[@]}"
echo "Building..."
cmake --build "${build_dir}" --config "${build_type}"
if [[ "${do_tests}" == "true" ]]; then
echo "Running tests..."
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
fi
if [[ "${do_package}" == "true" ]]; then
app_path="${build_dir}/Reclass.app"
if [[ ! -d "${app_path}" ]]; then
echo "ERROR: ${app_path} not found. Build may have failed." >&2
exit 1
fi
macdeployqt_bin=""
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
elif command -v macdeployqt >/dev/null 2>&1; then
macdeployqt_bin="$(command -v macdeployqt)"
fi
if [[ -z "${macdeployqt_bin}" ]]; then
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
exit 1
fi
echo "Running macdeployqt..."
"${macdeployqt_bin}" "${app_path}" -always-overwrite
arch="$(uname -m)"
zip_name="Reclass-macos-${arch}-qt6.zip"
echo "Creating zip: ${zip_name}"
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
echo "Packaged: ${build_dir}/${zip_name}"
fi

439
src/addressparser.cpp Normal file
View File

@@ -0,0 +1,439 @@
#include "addressparser.h"
namespace rcx {
// ── Address Expression Parser ──────────────────────────────────────────
//
// Parses expressions like:
// "7FF66CCE0000" → plain hex address
// "0x100 + 0x200" → arithmetic on hex values
// "<Program.exe> + 0xDE" → module base + offset
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
// "base + e_lfanew" → C/C++ style identifier resolution
// "0xFF & 0x0F" → bitwise AND
// "1 << 4" → shift left
//
// Grammar (C operator precedence):
//
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
// bitwiseAnd = shift ('&' shift)*
// shift = expr (('<<' | '>>') expr)*
// expr = term (('+' | '-') term)*
// term = unary (('*' | '/') unary)*
// unary = '-' unary | '~' unary | atom
// atom = '[' bitwiseOr ']' -- read pointer at address (dereference)
// | '<' moduleName '>' -- resolve module base address
// | '(' bitwiseOr ')' -- grouping
// | identifier -- C/C++ name resolved via callback
// | hexLiteral -- hex number, optional 0x prefix
//
// All numeric literals are hexadecimal (base 16).
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
class ExpressionParser {
public:
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
: m_input(input), m_callbacks(callbacks) {}
AddressParseResult parse() {
skipSpaces();
if (atEnd())
return error("empty expression");
uint64_t value = 0;
if (!parseBitwiseOr(value))
return error(m_error);
skipSpaces();
if (!atEnd())
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
return {true, value, {}, -1};
}
private:
const QString& m_input;
const AddressParserCallbacks* m_callbacks;
int m_pos = 0;
QString m_error;
int m_errorPos = 0;
// ── Helpers ──
bool atEnd() const { return m_pos >= m_input.size(); }
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
void advance() { m_pos++; }
void skipSpaces() {
while (!atEnd() && m_input[m_pos].isSpace())
m_pos++;
}
AddressParseResult error(const QString& msg) const {
return {false, 0, msg, m_errorPos};
}
bool fail(const QString& msg) {
m_error = msg;
m_errorPos = m_pos;
return false;
}
bool expect(QChar ch) {
skipSpaces();
if (peek() != ch)
return fail(QStringLiteral("expected '%1'").arg(ch));
advance();
return true;
}
static bool isHexDigit(QChar ch) {
return (ch >= '0' && ch <= '9')
|| (ch >= 'a' && ch <= 'f')
|| (ch >= 'A' && ch <= 'F');
}
static bool isIdentStart(QChar ch) {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
}
static bool isIdentChar(QChar ch) {
return isIdentStart(ch) || (ch >= '0' && ch <= '9');
}
// ── Recursive descent parsing ──
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
bool parseBitwiseOr(uint64_t& result) {
if (!parseBitwiseXor(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '|')
break;
advance();
uint64_t rhs = 0;
if (!parseBitwiseXor(rhs))
return false;
result |= rhs;
}
return true;
}
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
bool parseBitwiseXor(uint64_t& result) {
if (!parseBitwiseAnd(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '^')
break;
advance();
uint64_t rhs = 0;
if (!parseBitwiseAnd(rhs))
return false;
result ^= rhs;
}
return true;
}
// bitwiseAnd = shift ('&' shift)*
bool parseBitwiseAnd(uint64_t& result) {
if (!parseShift(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '&')
break;
advance();
uint64_t rhs = 0;
if (!parseShift(rhs))
return false;
result &= rhs;
}
return true;
}
// shift = expr (('<<' | '>>') expr)*
bool parseShift(uint64_t& result) {
if (!parseExpression(result))
return false;
for (;;) {
skipSpaces();
QChar c = peek();
if (c != '<' && c != '>')
break;
// Must be << or >> (not < or > alone)
if (m_pos + 1 >= m_input.size() || m_input[m_pos + 1] != c)
break;
bool isLeft = (c == '<');
advance(); advance(); // skip << or >>
uint64_t rhs = 0;
if (!parseExpression(rhs))
return false;
result = isLeft ? (result << rhs) : (result >> rhs);
}
return true;
}
// expr = term (('+' | '-') term)*
bool parseExpression(uint64_t& result) {
if (!parseTerm(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '+' && op != '-')
break;
advance();
uint64_t rhs = 0;
if (!parseTerm(rhs))
return false;
result = (op == '+') ? result + rhs : result - rhs;
}
return true;
}
// term = unary (('*' | '/') unary)*
bool parseTerm(uint64_t& result) {
if (!parseUnary(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '*' && op != '/')
break;
advance();
uint64_t rhs = 0;
if (!parseUnary(rhs))
return false;
if (op == '*') {
result *= rhs;
} else {
if (rhs == 0)
return fail("division by zero");
result /= rhs;
}
}
return true;
}
// unary = '-' unary | '~' unary | atom
bool parseUnary(uint64_t& result) {
skipSpaces();
if (peek() == '-') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
return true;
}
if (peek() == '~') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = ~inner;
return true;
}
return parseAtom(result);
}
// atom = '[' bitwiseOr ']' | '<' name '>' | '(' bitwiseOr ')' | identifier | hexLiteral
bool parseAtom(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
QChar ch = peek();
if (ch == '[') return parseDereference(result);
if (ch == '<') return parseModuleName(result);
if (ch == '(') return parseGrouping(result);
// Try identifier before hex — identifiers start with [a-zA-Z_]
if (isIdentStart(ch))
return parseIdentifierOrHex(result);
return parseHexNumber(result);
}
// Identifier or hex literal disambiguation.
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
// Otherwise → backtrack and parse as hex number.
bool parseIdentifierOrHex(uint64_t& result) {
int start = m_pos;
bool hasNonHex = false;
// Scan full token
while (!atEnd() && isIdentChar(peek())) {
if (!isHexDigit(peek()))
hasNonHex = true;
advance();
}
QString token = m_input.mid(start, m_pos - start);
if (!hasNonHex) {
// Pure hex digits (e.g. "DEAD") — backtrack, parse as hex
m_pos = start;
return parseHexNumber(result);
}
// It's an identifier — resolve via callback
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveIdentifier(token, &ok);
if (!ok)
return fail(QStringLiteral("unknown identifier '%1'").arg(token));
return true;
}
// '[' bitwiseOr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['
uint64_t address = 0;
if (!parseBitwiseOr(address))
return false;
if (!expect(']'))
return false;
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->readPointer) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->readPointer(address, &ok);
if (!ok)
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
return true;
}
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
bool parseModuleName(uint64_t& result) {
advance(); // skip '<'
int nameStart = m_pos;
while (!atEnd() && peek() != '>')
advance();
if (atEnd())
return fail("expected '>'");
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
advance(); // skip '>'
if (name.isEmpty())
return fail("empty module name");
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->resolveModule) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveModule(name, &ok);
if (!ok)
return fail(QStringLiteral("module '%1' not found").arg(name));
return true;
}
// '(' bitwiseOr ')' — parenthesized sub-expression for grouping
bool parseGrouping(uint64_t& result) {
advance(); // skip '('
if (!parseBitwiseOr(result))
return false;
return expect(')');
}
// Hex number with optional "0x" prefix. All literals are base-16.
bool parseHexNumber(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
int start = m_pos;
// Skip optional 0x/0X prefix
if (m_pos + 1 < m_input.size()
&& m_input[m_pos] == '0'
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
m_pos += 2;
// Consume hex digits
int digitsStart = m_pos;
while (!atEnd() && isHexDigit(peek()))
advance();
if (m_pos == digitsStart) {
m_errorPos = start;
return fail("expected hex number");
}
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
bool ok = false;
result = digits.toULongLong(&ok, 16);
if (!ok) {
m_errorPos = start;
return fail("invalid hex number");
}
return true;
}
};
// ── Public API ─────────────────────────────────────────────────────────
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
const AddressParserCallbacks* cb)
{
// ptrSize is used by the caller to configure the readPointer callback;
// the parser itself doesn't need it directly.
Q_UNUSED(ptrSize);
// WinDbg displays 64-bit addresses with backtick separators for readability,
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
// Also remove ' in case user uses it
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
ExpressionParser parser(cleaned, cb);
return parser.parse();
}
QString AddressParser::validate(const QString& formula)
{
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
cleaned = cleaned.trimmed();
if (cleaned.isEmpty())
return QStringLiteral("empty");
// Parse with no callbacks — modules, dereferences, identifiers succeed but return 0.
// This checks syntax only.
ExpressionParser parser(cleaned, nullptr);
auto result = parser.parse();
return result.ok ? QString() : result.error;
}
} // namespace rcx

28
src/addressparser.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
namespace rcx {
struct AddressParseResult {
bool ok;
uint64_t value;
QString error;
int errorPos;
};
struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
};
class AddressParser {
public:
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
const AddressParserCallbacks* cb = nullptr);
static QString validate(const QString& formula);
};
} // namespace rcx

View File

@@ -1,4 +1,5 @@
#include "core.h"
#include "addressparser.h"
#include <algorithm>
#include <numeric>
@@ -14,13 +15,16 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
struct ComposeState {
QString text;
QVector<LineMeta> meta;
QSet<uint64_t> visiting; // cycle detection for struct recursion
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
QSet<uint64_t> visiting; // cycle detection for struct recursion
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
int currentLine = 0;
int typeW = kColType; // global type column width (fallback)
int nameW = kColName; // global name column width (fallback)
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
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
// Precomputed for O(1) lookups
QHash<uint64_t, QVector<int>> childMap;
@@ -64,7 +68,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
uint64_t /*addr*/, bool isCont, int /*depth*/) {
uint32_t mask = 0;
if (isCont) mask |= (1u << M_CONT);
if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD);
// No ambient validation markers — errors only shown during inline editing.
return mask;
}
@@ -77,12 +80,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
}
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
if (tree.baseAddress == 0) return ptr;
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
return UINT64_MAX; // Invalid: ptr below base address
}
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
int64_t total = 0;
QSet<uint64_t> visited;
@@ -109,6 +106,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
return state.absOffsets[nodeIdx];
}
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
static const QVector<int> kEmpty;
auto it = state.childMap.constFind(parentId);
return it == state.childMap.constEnd() ? kEmpty : it.value();
}
void composeLeaf(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx,
int depth, uint64_t absAddr, uint64_t scopeId) {
@@ -118,23 +122,30 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
int typeW = state.effectiveTypeW(scopeId);
int nameW = state.effectiveNameW(scopeId);
// Line count: padding wraps at 8 bytes per line
int numLines;
if (node.kind == NodeKind::Padding) {
int totalBytes = qMax(1, node.arrayLen);
numLines = (totalBytes + 7) / 8;
} else {
numLines = linesForKind(node.kind);
}
int numLines = linesForKind(node.kind);
// Resolve pointer target name for display
QString ptrTypeOverride;
QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
// Primitive pointer: e.g. "int32*" or "f64**"
const auto* meta = kindMeta(node.elementKind);
QString baseName = meta ? QString::fromLatin1(meta->typeName)
: QStringLiteral("void");
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
ptrTypeOverride = baseName + stars;
} else {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
}
}
// Detect type overflow in compact mode (for effectiveTypeW)
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
int lineTypeW = typeOverflow ? rawType.size() : typeW;
for (int sub = 0; sub < numLines; sub++) {
bool isCont = (sub > 0);
@@ -146,26 +157,23 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.isContinuation = isCont;
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
lm.effectiveTypeW = lineTypeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
// Set byte count for hex preview lines (used for per-byte change highlighting)
if (isHexPreview(node.kind)) {
if (node.kind == NodeKind::Padding) {
int totalSz = qMax(1, node.arrayLen);
lm.lineByteCount = qMin(8, totalSz - sub * 8);
} else {
lm.lineByteCount = sizeForKind(node.kind);
}
lm.lineByteCount = sizeForKind(node.kind);
}
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
state.compactColumns);
state.emitLine(lineText, lm);
}
}
@@ -197,8 +205,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false);
@@ -215,8 +224,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::ArrayElementSeparator;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
@@ -244,16 +254,15 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.isRootHeader = false;
lm.foldHead = true;
lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = (1u << M_STRUCT_BG);
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
QString headerText;
if (node.kind == NodeKind::Array) {
@@ -264,24 +273,143 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.arrayCount = node.arrayLen;
QString elemStructName = (node.elementKind == NodeKind::Struct)
? resolvePointerTarget(tree, node.refId) : QString();
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName);
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
bool overflow = state.compactColumns && rawType.size() > typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
} else {
// All structs (root and nested) use the same header format
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
QString rawType = fmt::structTypeName(node);
bool overflow = state.compactColumns && rawType.size() > typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
}
state.emitLine(headerText, lm);
}
if (!node.collapsed || isArrayChild || isRootHeader) {
QVector<int> children = state.childMap.value(node.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
// Enum with members: render name = value lines instead of offset-based fields
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
int childDepth = depth + 1;
int maxNameLen = 4;
for (const auto& m : node.enumMembers)
maxNameLen = qMax(maxNameLen, (int)m.first.size());
// Build display order sorted by value
QVector<int> order(node.enumMembers.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](int a, int b) {
return node.enumMembers[a].second < node.enumMembers[b].second;
});
for (int oi = 0; oi < order.size(); oi++) {
int mi = order[oi];
const auto& m = node.enumMembers[mi];
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.subLine = mi;
lm.depth = childDepth;
lm.lineKind = LineKind::Field;
lm.isMemberLine = true;
lm.nodeKind = NodeKind::UInt32;
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.markerMask = 0;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
}
// Footer
if (!isArrayChild) {
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.isRootHeader = isRootHeader;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
}
state.visiting.remove(node.id);
return;
}
// Bitfield with members: render name : width = value lines
if (node.resolvedClassKeyword() == QStringLiteral("bitfield")
&& !node.bitfieldMembers.isEmpty()) {
int childDepth = depth + 1;
int maxNameLen = 4;
for (const auto& m : node.bitfieldMembers)
maxNameLen = qMax(maxNameLen, (int)m.name.size());
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
const auto& m = node.bitfieldMembers[mi];
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
m.bitOffset, m.bitWidth);
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.subLine = mi;
lm.depth = childDepth;
lm.lineKind = LineKind::Field;
lm.isMemberLine = true;
lm.nodeKind = node.elementKind;
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.markerMask = 0;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
childDepth, maxNameLen), lm);
}
// Footer
if (!isArrayChild) {
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.isRootHeader = isRootHeader;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
int sz = sizeForKind(node.elementKind);
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
state.visiting.remove(node.id);
return;
}
const QVector<int>& allChildren = childIndices(state, node.id);
// Split children into regular nodes and static fields (static fields render at the end)
QVector<int> regular, staticIdxs;
for (int ci : allChildren) {
if (tree.nodes[ci].isStatic)
staticIdxs.append(ci);
else
regular.append(ci);
}
int childDepth = depth + 1;
// Primitive arrays with no child nodes: synthesize element lines dynamically
if (node.kind == NodeKind::Array && children.isEmpty()
if (node.kind == NodeKind::Array && regular.isEmpty()
&& node.elementKind != NodeKind::Struct && node.elementKind != NodeKind::Array) {
int elemSize = sizeForKind(node.elementKind);
int eTW = state.effectiveTypeW(node.id);
@@ -307,21 +435,25 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind;
lm.isArrayElement = true;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + elemAddr;
lm.arrayElementIdx = i;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.effectiveTypeW = eTW;
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
lm.effectiveNameW = eNW;
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
{}, eTW, eNW, elemTypeStr), lm);
{}, eTW, eNW, elemTypeStr,
state.compactColumns), lm);
}
}
// Struct arrays with refId but no child nodes: synthesize by expanding the
// referenced struct for each element (like repeated pointer deref)
if (node.kind == NodeKind::Array && children.isEmpty()
if (node.kind == NodeKind::Array && regular.isEmpty()
&& node.elementKind == NodeKind::Struct && node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
@@ -338,13 +470,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
// Embedded struct with refId but no child nodes: expand referenced struct's
// children at this node's offset (single instance, like array with count=1)
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
if (node.kind == NodeKind::Struct && regular.isEmpty() && node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QVector<int> refChildren = state.childMap.value(node.refId);
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
const QVector<int>& 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) {
@@ -353,24 +482,27 @@ void composeParent(ComposeState& state, const NodeTree& tree,
if (state.visiting.contains(child.id)) {
int typeW = state.effectiveTypeW(refScopeId);
int nameW = state.effectiveNameW(refScopeId);
QString rawType = fmt::structTypeName(child);
bool overflow = state.compactColumns && rawType.size() > typeW;
LineMeta lm;
lm.nodeIdx = nodeIdx; // parent struct — materialize target
lm.nodeId = child.id;
lm.depth = childDepth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(
tree.baseAddress + absAddr + child.offset, false,
absAddr + child.offset, false,
state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
lm.offsetAddr = absAddr + child.offset;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = child.kind;
lm.foldHead = true;
lm.foldCollapsed = true;
lm.foldLevel = computeFoldLevel(childDepth, true);
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
lm.effectiveTypeW = typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
state.emitLine(fmt::fmtStructHeader(child, childDepth,
/*collapsed=*/true, typeW, nameW), lm);
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
continue;
}
composeNode(state, tree, prov, childIdx, childDepth,
@@ -382,7 +514,7 @@ 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 : children) {
for (int childIdx : regular) {
// 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,
@@ -390,6 +522,194 @@ void composeParent(ComposeState& state, const NodeTree& tree,
childrenAreArrayElements ? elementIdx++ : -1,
childrenAreArrayElements ? absAddr : 0);
}
// ── Static fields: render after regular children, before footer ──
if (!staticIdxs.isEmpty() && !node.collapsed) {
// Build identifier resolver for static field expressions
auto makeResolver = [&](uint64_t parentAbsAddr) {
AddressParserCallbacks cbs;
cbs.resolveIdentifier = [&tree, &prov, &regular, parentAbsAddr]
(const QString& name, bool* ok) -> uint64_t {
if (name == QStringLiteral("base")) {
*ok = true;
return parentAbsAddr;
}
// Find sibling field by name, read its value
for (int ci : regular) {
const Node& sib = tree.nodes[ci];
if (sib.name == name) {
int sz = sib.byteSize();
uint64_t sibAddr = parentAbsAddr + sib.offset;
if (sz > 0 && prov.isValid() && prov.isReadable(sibAddr, sz)) {
*ok = true;
if (sz == 1) return (uint64_t)prov.readU8(sibAddr);
if (sz == 2) return (uint64_t)prov.readU16(sibAddr);
if (sz == 4) return (uint64_t)prov.readU32(sibAddr);
return prov.readU64(sibAddr);
}
*ok = false;
return 0;
}
}
*ok = false;
return 0;
};
int ps = tree.pointerSize;
cbs.readPointer = [&prov, ps](uint64_t addr, bool* ok) -> uint64_t {
if (prov.isValid() && prov.isReadable(addr, ps)) {
*ok = true;
return (ps >= 8) ? prov.readU64(addr)
: (uint64_t)prov.readU32(addr);
}
*ok = false;
return 0;
};
return cbs;
};
auto cbs = makeResolver(absAddr);
for (int si : staticIdxs) {
const Node& sf = tree.nodes[si];
// Evaluate expression → absolute address
uint64_t staticAddr = 0;
bool exprOk = false;
if (!sf.offsetExpr.isEmpty()) {
auto result = AddressParser::evaluate(sf.offsetExpr, tree.pointerSize, &cbs);
exprOk = result.ok;
if (result.ok)
staticAddr = result.value;
}
// Resolve type name
QString typeName;
if (sf.kind == NodeKind::Struct)
typeName = fmt::structTypeName(sf);
else if (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)
typeName = fmt::pointerTypeName(sf.kind, resolvePointerTarget(tree, sf.refId));
else
typeName = fmt::typeNameRaw(sf.kind);
bool isCollapsed = sf.collapsed;
// ── Header line: "static <type> <name> {" or collapsed: "static <type> <name> { return <expr>; }"
QString headerLine;
if (isCollapsed) {
QString exprPart;
if (!sf.offsetExpr.isEmpty()) {
if (exprOk)
exprPart = QStringLiteral("return %1 } \u2192 0x%2")
.arg(sf.offsetExpr)
.arg(QString::number(staticAddr, 16).toUpper());
else
exprPart = QStringLiteral("return %1 } (error)").arg(sf.offsetExpr);
} else {
exprPart = QStringLiteral("}");
}
headerLine = fmt::indent(childDepth)
+ QStringLiteral("static ") + typeName
+ QStringLiteral(" ") + sf.name
+ QStringLiteral(" { ") + exprPart;
} else {
headerLine = fmt::indent(childDepth)
+ QStringLiteral("static ") + typeName
+ QStringLiteral(" ") + sf.name
+ QStringLiteral(" {");
}
LineMeta lm;
lm.nodeIdx = si;
lm.nodeId = sf.id;
lm.depth = childDepth;
lm.lineKind = LineKind::Header;
lm.nodeKind = sf.kind;
lm.foldHead = true;
lm.foldCollapsed = isCollapsed;
lm.isStaticLine = true;
lm.foldLevel = computeFoldLevel(childDepth, true);
lm.markerMask = (1u << M_STRUCT_BG);
lm.offsetText = QStringLiteral("~") + QString::number(staticAddr, 16)
.toUpper().rightJustified(state.offsetHexDigits - 1, '0');
lm.offsetAddr = staticAddr;
lm.ptrBase = state.currentPtrBase;
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
lm.effectiveNameW = sf.name.size();
state.emitLine(headerLine, lm);
// ── Body + children (only when expanded) ──
if (!isCollapsed) {
// Body line: " return <expr> → 0xADDR"
{
QString bodyLine;
if (!sf.offsetExpr.isEmpty()) {
if (exprOk)
bodyLine = fmt::indent(childDepth + 1)
+ QStringLiteral("return %1").arg(sf.offsetExpr);
else
bodyLine = fmt::indent(childDepth + 1)
+ QStringLiteral("return %1 (error)").arg(sf.offsetExpr);
} else {
bodyLine = fmt::indent(childDepth + 1)
+ QStringLiteral("return 0");
}
// Right-align resolved address
if (exprOk && !sf.offsetExpr.isEmpty()) {
bodyLine += QStringLiteral(" \u2192 0x")
+ QString::number(staticAddr, 16).toUpper();
}
LineMeta blm;
blm.nodeIdx = si;
blm.nodeId = sf.id;
blm.depth = childDepth + 1;
blm.lineKind = LineKind::Field;
blm.nodeKind = sf.kind;
blm.isStaticLine = true;
blm.foldLevel = computeFoldLevel(childDepth + 1, false);
blm.markerMask = 0;
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
blm.offsetAddr = staticAddr;
blm.ptrBase = state.currentPtrBase;
state.emitLine(bodyLine, blm);
}
// If struct/array, compose children at evaluated address
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
const QVector<int>& staticKids = childIndices(state, sf.id);
for (int sci : staticKids) {
composeNode(state, tree, prov, sci, childDepth + 1,
staticAddr, sf.id, false, sf.id);
}
}
// Footer line: "};"
{
LineMeta flm;
flm.nodeIdx = si;
flm.nodeId = sf.id;
flm.depth = childDepth;
flm.lineKind = LineKind::Footer;
flm.nodeKind = sf.kind;
flm.isStaticLine = true;
flm.foldLevel = computeFoldLevel(childDepth, false);
flm.markerMask = 0;
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
int sSpan = tree.structSpan(sf.id, &state.childMap);
flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false,
state.offsetHexDigits);
flm.offsetAddr = staticAddr + sSpan;
} else {
flm.offsetText = QString(state.offsetHexDigits, QChar(' '));
flm.offsetAddr = staticAddr;
}
flm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
}
}
}
}
}
// Footer line: skip when collapsed or for array element structs
@@ -404,8 +724,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap);
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + sz;
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
@@ -430,29 +751,44 @@ void composeNode(ComposeState& state, const NodeTree& tree,
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
// Check if this pointer has materialized children (from materializeRefChildren)
const QVector<int>& ptrChildren = childIndices(state, node.id);
bool hasMaterialized = !ptrChildren.isEmpty();
// Force collapsed if this refId is already being virtually expanded
// (prevents infinite recursion in virtual expansion mode).
// Materialized children bypass this — they are real tree nodes with
// independent collapsed state, so recursion is bounded by the tree.
bool forceCollapsed = !hasMaterialized
&& state.virtualPtrRefs.contains(node.refId);
bool effectiveCollapsed = node.collapsed || forceCollapsed;
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldHead = true;
lm.foldCollapsed = node.collapsed;
lm.foldCollapsed = effectiveCollapsed;
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
lm.effectiveTypeW = typeW;
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
prov, absAddr, ptrTypeOverride,
typeW, nameW), lm);
typeW, nameW, state.compactColumns), lm);
}
if (!node.collapsed) {
if (!effectiveCollapsed) {
int sz = node.byteSize();
uint64_t ptrVal = 0;
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
@@ -462,38 +798,59 @@ void composeNode(ComposeState& state, const NodeTree& tree,
// Treat sentinel values as invalid pointers
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0;
else {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
}
}
}
// Determine if pointer target is actually readable
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
// Pointer target address is used directly (absolute)
uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
// For invalid/unreadable pointers: use NullProvider (shows zeros)
// and reset margin offsets (unsigned wrap cancels baseAddress)
static NullProvider s_nullProv;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress;
pBase = 0;
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
if (!state.ptrVisiting.contains(key)) {
state.ptrVisiting.insert(key);
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
composeParent(state, tree, childProv, refIdx,
depth, pBase, ref.id,
/*isArrayChild=*/true);
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = pBase;
if (hasMaterialized) {
// 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,
pBase, node.id, false, node.id);
}
} else {
// Virtual expansion via ref struct definition.
// Temporarily remove the ref struct from visiting so composeParent
// doesn't hit the struct-level cycle guard. The ptrVisiting mechanism
// handles actual address-level pointer cycles, and virtualPtrRefs
// prevents infinite virtual recursion (inner self-referential pointers
// are force-collapsed with M_CYCLE for the user to materialize).
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
if (!state.ptrVisiting.contains(key)) {
state.ptrVisiting.insert(key);
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
bool wasVisiting = state.visiting.remove(node.refId);
state.virtualPtrRefs.insert(node.refId);
composeParent(state, tree, childProv, refIdx,
depth, pBase, ref.id,
/*isArrayChild=*/true);
state.virtualPtrRefs.remove(node.refId);
if (wasVisiting) state.visiting.insert(node.refId);
}
}
state.ptrVisiting.remove(key);
}
state.ptrVisiting.remove(key);
}
state.currentPtrBase = savedPtrBase;
// Footer for pointer fold
{
LineMeta lm;
@@ -520,23 +877,32 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns) {
ComposeState state;
state.compactColumns = compactColumns;
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)
state.childMap[tree.nodes[i].parentId].append(i);
// Precompute absolute offsets
for (auto it = state.childMap.begin(); it != state.childMap.end(); ++it) {
QVector<int>& children = it.value();
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
}
// Precompute absolute offsets (baseAddress + structure-relative offset)
state.absOffsets.resize(tree.nodes.size());
for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.computeOffset(i);
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
// Compute hex digit tier from max absolute address
{
uint64_t maxAddr = tree.baseAddress;
for (int i = 0; i < tree.nodes.size(); i++) {
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i];
uint64_t addr = (uint64_t)state.absOffsets[i];
if (addr > maxAddr) maxAddr = addr;
}
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
@@ -561,17 +927,18 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Compute effective type column width from longest type name
// Include struct/array headers which use "struct TypeName" or "type[count]" format
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
int maxTypeLen = kMinTypeW;
for (const Node& node : tree.nodes) {
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
}
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
// Compute effective name column width from longest name
// Include struct/array names - they now use columnar layout too
int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) {
// Skip hex/padding (they show ASCII preview, not name column)
// Skip hex (they show ASCII preview, not name column)
if (isHexPreview(node.kind)) continue;
maxNameLen = qMax(maxNameLen, (int)node.name.size());
}
@@ -590,7 +957,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
const Node& child = tree.nodes[childIdx];
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex/padding, but include containers)
// Name width (skip hex, but include containers)
if (!isHexPreview(child.kind)) {
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
}
@@ -609,7 +976,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
}
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
}
@@ -622,17 +989,17 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
const Node& child = tree.nodes[childIdx];
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex/padding, include containers)
// Name width (skip hex, include containers)
if (!isHexPreview(child.kind)) {
rootMaxName = qMax(rootMaxName, (int)child.name.size());
}
}
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
}
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {");
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {");
{
LineMeta lm;
lm.nodeIdx = -1;
@@ -643,16 +1010,14 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.foldHead = false;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
state.emitLine(cmdRowText, lm);
}
QVector<int> roots = state.childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
const QVector<int>& roots = childIndices(state, 0);
for (int idx : roots) {
// If viewRootId is set, skip roots that don't match
@@ -703,20 +1068,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
return result;
}
int NodeTree::computeStructAlignment(uint64_t structId) const {
int idx = indexOfId(structId);
if (idx < 0) return 1;
int maxAlign = 1;
QVector<int> kids = childrenOf(structId);
for (int ci : kids) {
const Node& c = nodes[ci];
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
} else {
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
}
}
return maxAlign;
}
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
#include <QUndoCommand>
#include <QTimer>
#include <QFutureWatcher>
#include <QPointer>
#include <memory>
namespace rcx {
@@ -39,7 +40,7 @@ public:
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
ComposeResult compose(uint64_t viewRootId = 0) const;
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -69,6 +70,7 @@ struct SavedSourceEntry {
QString filePath; // for File sources
QString providerTarget; // for plugin providers (e.g. "pid:name")
uint64_t baseAddress = 0;
QString baseAddressFormula;
};
// ── Controller ──
@@ -84,20 +86,32 @@ public:
void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; }
void convertRootKeyword(const QString& newKeyword);
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx);
void materializeRefChildren(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
void setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii = false, uint64_t resolvedAddr = 0);
void duplicateNode(int nodeIdx);
void convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId);
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
void editBitfieldValue(uint64_t nodeId, int memberIdx);
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void deleteRootStruct(uint64_t structId);
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
void dissolveUnion(uint64_t unionId);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
uint64_t findOrCreateStructByName(const QString& typeName);
// Selection
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
@@ -112,6 +126,9 @@ public:
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
void setCompactColumns(bool v);
void resetProvider();
// MCP bridge accessors
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
@@ -119,6 +136,21 @@ public:
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
int activeSourceIndex() const { return m_activeSourceIdx; }
void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }
void setTrackValues(bool on);
void resetChangeTracking();
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
// Test accessors
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
const ComposeResult& lastResult() const { return m_lastResult; }
signals:
void nodeSelected(int nodeIdx);
@@ -131,6 +163,7 @@ private:
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
bool m_suppressRefresh = false;
bool m_compactColumns = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──
@@ -138,7 +171,8 @@ private:
int m_activeSourceIdx = -1;
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
TypeSelectorPopup* m_cachedPopup = nullptr;
QPointer<TypeSelectorPopup> m_cachedPopup;
int m_typePopupGen = 0; // generation counter for deferred content loading
// ── Auto-refresh state ──
using PageMap = QHash<uint64_t, QByteArray>;
@@ -147,18 +181,20 @@ private:
std::unique_ptr<SnapshotProvider> m_snapshotProv;
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = true;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;
QVector<RcxDocument*>* m_projectDocs = nullptr;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
void performRealignment(uint64_t structId, int targetAlign);
void switchToSavedSource(int idx);
void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
// ── Auto-refresh methods ──
@@ -169,7 +205,7 @@ private:
void resetSnapshot();
void collectPointerRanges(uint64_t structId, uint64_t memBase,
int depth, int maxDepth,
QSet<uint64_t>& visited,
QSet<QPair<uint64_t,uint64_t>>& visited,
QVector<QPair<uint64_t,int>>& ranges) const;
};

View File

@@ -8,8 +8,10 @@
#include <QHash>
#include <QSet>
#include <cstdint>
#include <array>
#include <memory>
#include <variant>
#include <QDateTime>
#include "providers/provider.h"
#include "providers/buffer_provider.h"
@@ -25,23 +27,23 @@ enum class NodeKind : uint8_t {
UInt8, UInt16, UInt32, UInt64,
Float, Double, Bool,
Pointer32, Pointer64,
FuncPtr32, FuncPtr64,
Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16,
Padding,
Struct, Array
};
} // namespace rcx (temporarily close for qHash)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast<uint8_t>(key), seed); }
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
#endif
namespace rcx { // reopen
// ── Kind flags (replaces repeated Hex/Padding switches) ──
// ── Kind flags (replaces repeated Hex switches) ──
enum KindFlags : uint32_t {
KF_None = 0,
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout)
KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
KF_Container = 1 << 1, // Struct/Array
KF_String = 1 << 2, // UTF8/UTF16
KF_Vector = 1 << 3, // Vec2/3/4
@@ -78,13 +80,14 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::FuncPtr32, "FuncPtr32", "fnptr32", 4, 1, 4, KF_None},
{NodeKind::FuncPtr64, "FuncPtr64", "fnptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
{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::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
};
@@ -137,6 +140,18 @@ inline constexpr bool isVectorKind(NodeKind k) {
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline constexpr bool isFuncPtr(NodeKind k) {
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
}
// Hex types, pointer types, function pointers, and containers are not meaningful
// primitive-pointer targets — dereferencing them produces the same output as void*.
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
if (isHexNode(k)) return false;
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
if (isFuncPtr(k)) return false;
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
return true;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out;
@@ -155,7 +170,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
enum Marker : int {
M_CONT = 0,
M_PAD = 1,
M_PTR0 = 2,
M_CYCLE = 3,
M_ERR = 4,
@@ -166,6 +180,14 @@ enum Marker : int {
M_ACCENT = 9,
};
// ── Bitfield member (name + bit position + width within a container) ──
struct BitfieldMember {
QString name;
uint8_t bitOffset = 0; // position from LSB within the container
uint8_t bitWidth = 1; // number of bits (1..64)
};
// ── Node ──
struct Node {
@@ -176,20 +198,34 @@ struct Node {
QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0;
bool isStatic = false; // static field — excluded from struct layout
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
int arrayLen = 1; // Array: element count
int strLen = 64;
bool collapsed = false;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
NodeKind elementKind = NodeKind::UInt8; // Array: element type
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
int viewIndex = 0; // Array: current view offset (transient)
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
QVector<BitfieldMember> bitfieldMembers; // Bitfield: per-bit member definitions
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
int byteSize() const {
switch (kind) {
case NodeKind::UTF8: return strLen;
case NodeKind::UTF16: return strLen * 2;
case NodeKind::Padding: return qMax(1, arrayLen);
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
case NodeKind::Array: {
int elemSz = sizeForKind(elementKind);
if (elemSz <= 0) return 0;
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
}
case NodeKind::Struct:
if (classKeyword == QStringLiteral("bitfield")) {
int sz = sizeForKind(elementKind);
return sz > 0 ? sz : 4;
}
return 0;
default: return sizeForKind(kind);
}
}
@@ -205,11 +241,38 @@ struct Node {
o["classKeyword"] = classKeyword;
o["parentId"] = QString::number(parentId);
o["offset"] = offset;
if (isStatic)
o["isStatic"] = true;
if (!offsetExpr.isEmpty())
o["offsetExpr"] = offsetExpr;
o["arrayLen"] = arrayLen;
o["strLen"] = strLen;
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
if (!enumMembers.isEmpty()) {
QJsonArray arr;
for (const auto& m : enumMembers) {
QJsonObject em;
em["name"] = m.first;
em["value"] = QString::number(m.second);
arr.append(em);
}
o["enumMembers"] = arr;
}
if (!bitfieldMembers.isEmpty()) {
QJsonArray arr;
for (const auto& m : bitfieldMembers) {
QJsonObject bm;
bm["name"] = m.name;
bm["bitOffset"] = m.bitOffset;
bm["bitWidth"] = m.bitWidth;
arr.append(bm);
}
o["bitfieldMembers"] = arr;
}
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -221,11 +284,33 @@ struct Node {
n.classKeyword = o["classKeyword"].toString();
n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0);
n.arrayLen = o["arrayLen"].toInt(1);
n.strLen = o["strLen"].toInt(64);
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
n.offsetExpr = o["offsetExpr"].toString();
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
if (o.contains("enumMembers")) {
QJsonArray arr = o["enumMembers"].toArray();
for (const auto& v : arr) {
QJsonObject em = v.toObject();
n.enumMembers.append({em["name"].toString(),
em["value"].toString("0").toLongLong()});
}
}
if (o.contains("bitfieldMembers")) {
QJsonArray arr = o["bitfieldMembers"].toArray();
for (const auto& v : arr) {
QJsonObject bm = v.toObject();
BitfieldMember m;
m.name = bm["name"].toString();
m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0);
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
n.bitfieldMembers.append(m);
}
}
return n;
}
@@ -247,6 +332,8 @@ struct Node {
struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
@@ -360,6 +447,7 @@ struct NodeTree {
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
for (int ci : kids) {
const Node& c = nodes[ci];
if (c.isStatic) continue; // static fields don't affect struct size
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
? structSpan(c.id, childMap, visited) : c.byteSize();
int end = c.offset + sz;
@@ -373,9 +461,6 @@ struct NodeTree {
return qMax(declaredSize, maxEnd);
}
// Compute natural alignment of a struct (max alignment of direct children)
int computeStructAlignment(uint64_t structId) const;
// Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
@@ -383,6 +468,10 @@ struct NodeTree {
QJsonObject toJson() const {
QJsonObject o;
o["baseAddress"] = QString::number(baseAddress, 16);
if (!baseAddressFormula.isEmpty())
o["baseAddressFormula"] = baseAddressFormula;
if (pointerSize != 8)
o["pointerSize"] = pointerSize;
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
@@ -393,6 +482,8 @@ struct NodeTree {
static NodeTree fromJson(const QJsonObject& o) {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.baseAddressFormula = o["baseAddressFormula"].toString();
t.pointerSize = o["pointerSize"].toInt(8);
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
for (const auto& v : arr) {
@@ -405,6 +496,66 @@ struct NodeTree {
};
// ── Value History (ring buffer for heatmap) ──
struct ValueHistory {
static constexpr int kCapacity = 10;
std::array<QString, kCapacity> values;
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
int count = 0; // total unique values recorded
int head = 0; // next write position in ring
void record(const QString& v) {
if (count > 0) {
int last = (head + kCapacity - 1) % kCapacity;
if (values[last] == v) return; // no change
}
values[head] = v;
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
head = (head + 1) % kCapacity;
if (count < INT_MAX) count++;
}
void clear() {
count = 0;
head = 0;
}
int uniqueCount() const { return qMin(count, kCapacity); }
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
int heatLevel() const {
if (count <= 1) return 0;
if (count == 2) return 1;
if (count <= 4) return 2;
return 3;
}
QString last() const {
if (count == 0) return {};
return values[(head + kCapacity - 1) % kCapacity];
}
// Iterate from oldest to newest (up to uniqueCount entries)
template<typename Fn>
void forEach(Fn&& fn) const {
int n = uniqueCount();
int start = (head + kCapacity - n) % kCapacity;
for (int i = 0; i < n; i++)
fn(values[(start + i) % kCapacity]);
}
// Iterate with timestamps from newest to oldest
template<typename Fn>
void forEachWithTime(Fn&& fn) const {
int n = uniqueCount();
for (int i = 0; i < n; i++) {
int idx = (head + kCapacity - 1 - i) % kCapacity;
fn(values[idx], timestamps[idx]);
}
}
};
// ── LineMeta ──
enum class LineKind : uint8_t {
@@ -417,6 +568,29 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
}
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
static constexpr uint64_t kMemberSubShift = 48;
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
}
inline int memberSubFromSelId(uint64_t selId) {
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
}
struct LineMeta {
int nodeIdx = -1;
@@ -437,14 +611,18 @@ struct LineMeta {
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
QString offsetText;
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
uint32_t markerMask = 0;
bool dataChanged = false; // true if any byte in this node changed since last refresh
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
int lineByteCount = 0; // Hex preview: actual data byte count on this line
int effectiveTypeW = 14; // Per-line type column width used for rendering
int effectiveNameW = 22; // Per-line name column width used for rendering
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
bool isArrayElement = false; // true for synthesized primitive array element lines
bool isMemberLine = false; // true for enum member / bitfield member lines
bool isStaticLine = false; // true for static field node lines
};
inline bool isSyntheticLine(const LineMeta& lm) {
@@ -479,7 +657,7 @@ namespace cmd {
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
struct Remove { uint64_t nodeId; QVector<Node> subtree;
QVector<OffsetAdj> offAdjs; };
struct ChangeBase { uint64_t oldBase, newBase; };
struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; };
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
struct ChangeArrayMeta { uint64_t nodeId;
NodeKind oldElementKind, newElementKind;
@@ -489,13 +667,18 @@ namespace cmd {
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
struct ChangeEnumMembers { uint64_t nodeId;
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
}
using Command = std::variant<
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
cmd::ChangeClassKeyword, cmd::ChangeOffset
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
cmd::ChangeOffsetExpr, cmd::ToggleStatic
>;
// ── Column spans (for inline editing) ──
@@ -508,7 +691,7 @@ struct ColumnSpan {
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
ArrayElementType, ArrayElementCount, PointerTarget,
RootClassType, RootClassName, TypeSelector };
RootClassType, RootClassName, TypeSelector, StaticExpr };
// Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
@@ -522,20 +705,21 @@ inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uin
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 128; // Maximum name column width
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
return {ind, ind + typeW, true};
}
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
int start = ind + typeW + kSepWidth;
// Hex/Padding: ASCII preview occupies the name column (padded to nameW)
// Hex: ASCII preview occupies the name column (padded to nameW)
if (isHexPreview(lm.nodeKind))
return {start, start + nameW, true};
@@ -545,11 +729,12 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
if (lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
// Hex/Padding uses nameW for ASCII column (same as regular name column)
bool isHexPad = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue;
// Hex uses nameW for ASCII column (same as regular name column)
bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth;
@@ -563,12 +748,51 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
return {start, start + valWidth, true};
}
// Member line spans (enum "name = value", bitfield "name : N = value")
inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) {
if (!lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
int eq = lineText.indexOf(QLatin1String(" = "), ind);
if (eq < 0) return {};
int nameEnd = eq;
while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--;
return {ind, nameEnd, true};
}
inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) {
if (!lm.isMemberLine) return {};
int eq = lineText.indexOf(QLatin1String(" = "));
if (eq < 0) return {};
int valStart = eq + 3;
int valEnd = lineText.size();
while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--;
return {valStart, valEnd, true};
}
// Static field expression span: locates text between "return " and "→" / "(error)" / end
inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) {
int ret = lineText.indexOf(QLatin1String("return "));
if (ret < 0) return {};
int exprStart = ret + 7;
// End: before arrow, before "(error)", or line end
int exprEnd = lineText.size();
int arrow = lineText.indexOf(QChar(0x2192), exprStart);
if (arrow > exprStart) exprEnd = arrow;
int err = lineText.indexOf(QLatin1String("(error)"), exprStart);
if (err > exprStart && err < exprEnd) exprEnd = err;
// Also stop at " }" for collapsed format
int brace = lineText.indexOf(QLatin1String(" }"), exprStart);
if (brace > exprStart && brace < exprEnd) exprEnd = brace;
while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--;
return {exprStart, exprEnd, true};
}
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3;
bool isHexPad = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue;
bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth;
int start;
@@ -584,50 +808,56 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
// Line format: "source▾ · 0x140000000"
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
if (idx < 0) return {};
// Source label ends at the ▾ dropdown arrow
int arrow = lineText.indexOf(QChar(0x25BE));
if (arrow < 0) return {};
int start = 0;
while (start < idx && !lineText[start].isLetterOrNumber()
while (start < arrow && !lineText[start].isLetterOrNumber()
&& lineText[start] != '<' && lineText[start] != '\'') start++;
if (start >= idx) return {};
// Exclude trailing ▾ from the editable span
int end = idx;
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
if (end <= start) return {};
return {start, end, true};
}
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
if (tag < 0) return {};
int start = tag + 3; // after " · "
int end = start;
while (end < lineText.size() && !lineText[end].isSpace()) end++;
if (end <= start) return {};
return {start, end, true};
if (start >= arrow) return {};
return {start, arrow, true};
}
// ── CommandRow root-class spans ──
// Combined CommandRow format ends with: " struct ClassName {"
// Combined CommandRow format ends with: " struct ClassName {"
inline int commandRowRootStart(const QString& lineText) {
int best = -1;
int i;
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
// Match "struct " / "class " / "enum " as whole words before the class name
i = lineText.lastIndexOf(QStringLiteral("struct "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("class "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("enum "));
if (i > best) best = i;
return best;
}
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
// Address starts at "0x" after the source dropdown arrow
int arrow = lineText.indexOf(QChar(0x25BE));
if (arrow < 0) return {};
int start = lineText.indexOf(QStringLiteral("0x"), arrow);
if (start < 0) {
// Formula mode: address is between arrow and root keyword
start = arrow + 1;
while (start < lineText.size() && lineText[start].isSpace()) start++;
}
// End at root keyword (struct/class/enum) or end of line
int rootStart = commandRowRootStart(lineText);
int end = (rootStart > start) ? rootStart : lineText.size();
// Trim trailing whitespace
while (end > start && lineText[end - 1].isSpace()) end--;
if (end <= start) return {};
return {start, end, true};
}
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
int start = commandRowRootStart(lineText);
if (start < 0) return {};
int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ')
&& lineText[end] != QChar(0x25BE)) end++;
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
if (end <= start) return {};
return {start, end, true};
}
@@ -771,17 +1001,18 @@ namespace fmt {
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0,
const QString& comment = {}, int colType = kColType, int colName = kColName,
const QString& typeOverride = {});
const QString& typeOverride = {}, bool compact = false);
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {});
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
QString structTypeName(const Node& node); // Full type string for struct headers
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
QString pointerTypeName(NodeKind kind, const QString& targetName);
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
bool compact = false);
QString validateBaseAddress(const QString& text);
QString indent(int depth);
QString readValue(const Node& node, const Provider& prov,
@@ -791,10 +1022,17 @@ namespace fmt {
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
QString validateValue(NodeKind kind, const QString& text);
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
uint64_t value, int depth, int nameW);
uint64_t extractBits(const Provider& prov, uint64_t addr,
NodeKind containerKind,
uint8_t bitOffset, uint8_t bitWidth);
} // namespace fmt
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false);
} // namespace rcx

76
src/disasm.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include "disasm.h"
extern "C" {
#include <fadec.h>
}
namespace rcx {
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes) {
if (bytes.isEmpty() || (bitness != 32 && bitness != 64))
return {};
int len = qMin((int)bytes.size(), maxBytes);
const auto* buf = reinterpret_cast<const uint8_t*>(bytes.constData());
QString result;
int off = 0;
while (off < len) {
FdInstr instr;
int ret = fd_decode(buf + off, len - off, bitness, baseAddr + off, &instr);
if (ret < 0)
break;
char fmtBuf[128];
fd_format(&instr, fmtBuf, sizeof(fmtBuf));
if (!result.isEmpty())
result += QLatin1Char('\n');
result += QStringLiteral("%1 %2")
.arg(baseAddr + off, bitness == 64 ? 16 : 8, 16, QLatin1Char('0'))
.arg(QString::fromLatin1(fmtBuf));
off += ret;
}
return result;
}
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes) {
if (bytes.isEmpty())
return {};
int len = qMin((int)bytes.size(), maxBytes);
QString result;
for (int off = 0; off < len; off += 16) {
int lineLen = qMin(16, len - off);
if (!result.isEmpty())
result += QLatin1Char('\n');
// Address
bool wide = (baseAddr + len > 0xFFFFFFFFULL);
result += QStringLiteral("%1 ").arg(baseAddr + off, wide ? 16 : 8, 16, QLatin1Char('0'));
// Hex bytes
for (int i = 0; i < 16; i++) {
if (i < lineLen) {
uint8_t b = static_cast<uint8_t>(bytes[off + i]);
result += QStringLiteral("%1 ").arg(b, 2, 16, QLatin1Char('0'));
} else {
result += QStringLiteral(" ");
}
if (i == 7) result += QLatin1Char(' ');
}
// ASCII
result += QLatin1Char(' ');
for (int i = 0; i < lineLen; i++) {
char c = bytes[off + i];
result += (c >= 0x20 && c < 0x7f) ? QLatin1Char(c) : QLatin1Char('.');
}
}
return result;
}
} // namespace rcx

15
src/disasm.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <QString>
#include <QByteArray>
#include <cstdint>
namespace rcx {
// Disassemble up to maxBytes of x86 code, returning formatted asm lines.
// bitness: 32 or 64. Returns one line per instruction, prefixed with offset.
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes = 128);
// Format bytes as hex dump lines (16 bytes per line with ASCII sidebar).
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes = 128);
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,9 @@
#include <QWidget>
#include <QSet>
#include <QPoint>
#include <QHash>
class QLineEdit;
class QsciScintilla;
class QsciLexerCPP;
@@ -27,9 +29,12 @@ public:
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId);
void showFindBar();
void dismissHistoryPopup();
// ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
@@ -43,6 +48,7 @@ public:
bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit();
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
void applySelectionOverlay(const QSet<uint64_t>& selIds);
void setCommandRowText(const QString& line);
@@ -54,6 +60,12 @@ public:
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
QString textWithMargins() const;
void setCustomTypeNames(const QStringList& names);
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
}
void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); }
// Saved sources for quick-switch in source picker
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
@@ -61,12 +73,15 @@ public:
signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled();
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
@@ -78,7 +93,7 @@ private:
LayoutInfo m_layout; // cached from ComposeResult
// ── Toggle: absolute vs relative offset margin
bool m_relativeOffsets = false;
bool m_relativeOffsets = true;
int m_marginStyleBase = -1;
int m_hintLine = -1;
@@ -88,8 +103,12 @@ private:
bool m_hoverInside = false;
uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1;
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
int m_prevHoveredLine = -1; // for incremental marker update
QSet<uint64_t> m_currentSelIds;
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
// ── nodeId → display-line index (built in applyDocument) ──
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
// ── Drag selection ──
bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded
@@ -119,6 +138,7 @@ private:
bool lastValidationOk = true; // track state to avoid redundant updates
};
InlineEditState m_editState;
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
// ── Tab cycling state ──
EditTarget m_lastTabTarget = EditTarget::Value;
@@ -129,7 +149,23 @@ private:
// ── Saved sources for quick-switch ──
QVector<SavedSourceDisplay> m_savedSourceDisplay;
// ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;
// ── Find bar ──
QWidget* m_findBarContainer = nullptr;
QLineEdit* m_findBar = nullptr;
long m_findPos = 0;
void hideFindBar();
// ── Reentrancy guards ──
bool m_applyingDocument = false;
bool m_clampingSelection = false;
bool m_updatingComment = false;
@@ -145,7 +181,8 @@ private:
void applyMarkers(const QVector<LineMeta>& meta);
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyHexDimming(const QVector<LineMeta>& meta);
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
void applySymbolColoring(const QVector<LineMeta>& meta);
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
void applyCommandRowPills();

356
src/examples/EPROCESS.rcx Normal file
View File

@@ -0,0 +1,356 @@
{
"baseAddress": "FFFF800000000000",
"nextId": "9000",
"nodes": [
{"id":"100","kind":"Struct","name":"list_entry","structTypeName":"_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"101","kind":"Pointer64","name":"Flink","offset":0,"parentId":"100","refId":"100","collapsed":true},
{"id":"102","kind":"Pointer64","name":"Blink","offset":8,"parentId":"100","refId":"100","collapsed":true},
{"id":"110","kind":"Struct","name":"single_list_entry","structTypeName":"_SINGLE_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"111","kind":"Pointer64","name":"Next","offset":0,"parentId":"110","refId":"110","collapsed":true},
{"id":"120","kind":"Struct","name":"ex_push_lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"121","kind":"Hex64","name":"Value","offset":0,"parentId":"120"},
{"id":"130","kind":"Struct","name":"ex_rundown_ref","structTypeName":"_EX_RUNDOWN_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"131","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"130","refId":"0","collapsed":false},
{"id":"132","kind":"UInt64","name":"Count","offset":0,"parentId":"131"},
{"id":"133","kind":"Pointer64","name":"Ptr","offset":0,"parentId":"131"},
{"id":"140","kind":"Struct","name":"ex_fast_ref","structTypeName":"_EX_FAST_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"141","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"140","refId":"0","collapsed":false},
{"id":"142","kind":"Pointer64","name":"Object","offset":0,"parentId":"141"},
{"id":"143","kind":"UInt64","name":"Value","offset":0,"parentId":"141"},
{"id":"150","kind":"Struct","name":"unicode_string","structTypeName":"_UNICODE_STRING","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"151","kind":"UInt16","name":"Length","offset":0,"parentId":"150"},
{"id":"152","kind":"UInt16","name":"MaximumLength","offset":2,"parentId":"150"},
{"id":"153","kind":"Pointer64","name":"Buffer","offset":8,"parentId":"150"},
{"id":"160","kind":"Struct","name":"large_integer","structTypeName":"_LARGE_INTEGER","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"161","kind":"Struct","name":"","offset":0,"parentId":"160","refId":"0","collapsed":false},
{"id":"162","kind":"UInt32","name":"LowPart","offset":0,"parentId":"161"},
{"id":"163","kind":"Int32","name":"HighPart","offset":4,"parentId":"161"},
{"id":"164","kind":"Int64","name":"QuadPart","offset":0,"parentId":"160"},
{"id":"170","kind":"Struct","name":"rtl_avl_tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"171","kind":"Pointer64","name":"Root","offset":0,"parentId":"170"},
{"id":"180","kind":"Struct","name":"kstack_count","structTypeName":"_KSTACK_COUNT","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"181","kind":"Int32","name":"Value","offset":0,"parentId":"180"},
{"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"},
{"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"191","kind":"Struct","name":"","offset":0,"parentId":"190","refId":"0","collapsed":false},
{"id":"192","kind":"UInt8","name":"ExecuteDisable","offset":0,"parentId":"191"},
{"id":"193","kind":"Hex8","name":"ExecuteDisable:1 ExecuteEnable:1 DisableThunkEmulation:1 Permanent:1 ExecuteDispatchEnable:1 ImageDispatchEnable:1 DisableExceptionChainValidation:1 Spare:1","offset":0,"parentId":"191"},
{"id":"194","kind":"UInt8","name":"ExecuteOptions","offset":0,"parentId":"190"},
{"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"},
{"id":"210","kind":"Struct","name":"ps_protection","structTypeName":"_PS_PROTECTION","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"211","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"210","refId":"0","collapsed":false},
{"id":"212","kind":"UInt8","name":"Level","offset":0,"parentId":"211"},
{"id":"213","kind":"Hex8","name":"Type:3 Audit:1 Signer:4","offset":0,"parentId":"211"},
{"id":"220","kind":"Struct","name":"timer_delay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"221","kind":"Hex64","name":"DelayMs:30 CoalescingWindowMs:30 Reserved:1 NewTimerWheel:1 Retry:1 Locked:1","offset":0,"parentId":"220"},
{"id":"222","kind":"UInt64","name":"All","offset":0,"parentId":"220"},
{"id":"230","kind":"Struct","name":"wnf_state_name","structTypeName":"_WNF_STATE_NAME","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"231","kind":"UInt32","name":"Data_0","offset":0,"parentId":"230"},
{"id":"232","kind":"UInt32","name":"Data_1","offset":4,"parentId":"230"},
{"id":"240","kind":"Struct","name":"dynamic_ranges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"241","kind":"Struct","name":"Tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"240","refId":"170","collapsed":true},
{"id":"242","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":8,"parentId":"240","refId":"120","collapsed":true},
{"id":"250","kind":"Struct","name":"alpc_context","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"251","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"250","refId":"120","collapsed":true},
{"id":"252","kind":"Struct","name":"ViewListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"250","refId":"100","collapsed":true},
{"id":"253","kind":"UInt64","name":"PagedPoolQuotaCache","offset":24,"parentId":"250"},
{"id":"260","kind":"Struct","name":"mmsupport_flags","structTypeName":"_MMSUPPORT_FLAGS","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"261","kind":"Hex32","name":"EntireFlags","offset":0,"parentId":"260"},
{"id":"270","kind":"Struct","name":"mmsupport_shared","structTypeName":"_MMSUPPORT_SHARED","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"271","kind":"Pointer64","name":"WorkingSetLockArray","offset":0,"parentId":"270"},
{"id":"272","kind":"UInt64","name":"ReleasedCommitDebt","offset":8,"parentId":"270"},
{"id":"273","kind":"UInt64","name":"ResetPagesRepurposedCount","offset":16,"parentId":"270"},
{"id":"274","kind":"Pointer64","name":"WsSwapSupport","offset":24,"parentId":"270"},
{"id":"275","kind":"Pointer64","name":"CommitReleaseContext","offset":32,"parentId":"270"},
{"id":"276","kind":"Pointer64","name":"AccessLog","offset":40,"parentId":"270"},
{"id":"277","kind":"UInt64","name":"ChargedWslePages","offset":48,"parentId":"270"},
{"id":"278","kind":"UInt64","name":"ActualWslePages","offset":56,"parentId":"270"},
{"id":"279","kind":"Int32","name":"WorkingSetCoreLock","offset":64,"parentId":"270"},
{"id":"280","kind":"Pointer64","name":"ShadowMapping","offset":72,"parentId":"270"},
{"id":"300","kind":"Struct","name":"mmsupport_instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"301","kind":"UInt32","name":"NextPageColor","offset":0,"parentId":"300"},
{"id":"302","kind":"UInt32","name":"PageFaultCount","offset":4,"parentId":"300"},
{"id":"303","kind":"UInt64","name":"TrimmedPageCount","offset":8,"parentId":"300"},
{"id":"304","kind":"Pointer64","name":"VmWorkingSetList","offset":16,"parentId":"300"},
{"id":"305","kind":"Struct","name":"WorkingSetExpansionLinks","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"300","refId":"100","collapsed":true},
{"id":"306","kind":"UInt64","name":"AgeDistribution_0","offset":40,"parentId":"300"},
{"id":"307","kind":"UInt64","name":"AgeDistribution_1","offset":48,"parentId":"300"},
{"id":"308","kind":"UInt64","name":"AgeDistribution_2","offset":56,"parentId":"300"},
{"id":"309","kind":"UInt64","name":"AgeDistribution_3","offset":64,"parentId":"300"},
{"id":"310","kind":"UInt64","name":"AgeDistribution_4","offset":72,"parentId":"300"},
{"id":"311","kind":"UInt64","name":"AgeDistribution_5","offset":80,"parentId":"300"},
{"id":"312","kind":"UInt64","name":"AgeDistribution_6","offset":88,"parentId":"300"},
{"id":"313","kind":"UInt64","name":"AgeDistribution_7","offset":96,"parentId":"300"},
{"id":"314","kind":"Pointer64","name":"ExitOutswapGate","offset":104,"parentId":"300"},
{"id":"315","kind":"UInt64","name":"MinimumWorkingSetSize","offset":112,"parentId":"300"},
{"id":"316","kind":"UInt64","name":"MaximumWorkingSetSize","offset":120,"parentId":"300"},
{"id":"317","kind":"UInt64","name":"WorkingSetLeafSize","offset":128,"parentId":"300"},
{"id":"318","kind":"UInt64","name":"WorkingSetLeafPrivateSize","offset":136,"parentId":"300"},
{"id":"319","kind":"UInt64","name":"WorkingSetSize","offset":144,"parentId":"300"},
{"id":"320","kind":"UInt64","name":"WorkingSetPrivateSize","offset":152,"parentId":"300"},
{"id":"321","kind":"UInt64","name":"PeakWorkingSetSize","offset":160,"parentId":"300"},
{"id":"322","kind":"UInt32","name":"HardFaultCount","offset":168,"parentId":"300"},
{"id":"323","kind":"UInt16","name":"LastTrimStamp","offset":172,"parentId":"300"},
{"id":"324","kind":"UInt16","name":"PartitionId","offset":174,"parentId":"300"},
{"id":"325","kind":"UInt64","name":"SelfmapLock","offset":176,"parentId":"300"},
{"id":"326","kind":"Struct","name":"Flags","structTypeName":"_MMSUPPORT_FLAGS","offset":184,"parentId":"300","refId":"260","collapsed":true},
{"id":"350","kind":"Struct","name":"mmsupport_full","structTypeName":"_MMSUPPORT_FULL","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"351","kind":"Struct","name":"Instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"350","refId":"300","collapsed":true},
{"id":"352","kind":"Struct","name":"Shared","structTypeName":"_MMSUPPORT_SHARED","offset":192,"parentId":"350","refId":"270","collapsed":true},
{"id":"400","kind":"Struct","name":"dispatcher_header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"401","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"400","refId":"0","collapsed":false},
{"id":"402","kind":"UInt8","name":"Type","offset":0,"parentId":"401"},
{"id":"403","kind":"UInt8","name":"Signalling","offset":1,"parentId":"401"},
{"id":"404","kind":"UInt8","name":"Size","offset":2,"parentId":"401"},
{"id":"405","kind":"UInt8","name":"Reserved1","offset":3,"parentId":"401"},
{"id":"406","kind":"Int32","name":"Lock","offset":0,"parentId":"401"},
{"id":"407","kind":"Int32","name":"SignalState","offset":4,"parentId":"400"},
{"id":"408","kind":"Struct","name":"WaitListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"400","refId":"100","collapsed":true},
{"id":"500","kind":"Struct","name":"kprocess","structTypeName":"_KPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"501","kind":"Struct","name":"Header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"500","refId":"400","collapsed":true},
{"id":"502","kind":"Struct","name":"ProfileListHead","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"500","refId":"100","collapsed":true},
{"id":"503","kind":"UInt64","name":"DirectoryTableBase","offset":40,"parentId":"500"},
{"id":"504","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":48,"parentId":"500","refId":"100","collapsed":true},
{"id":"505","kind":"UInt32","name":"ProcessLock","offset":64,"parentId":"500"},
{"id":"506","kind":"UInt32","name":"ProcessTimerDelay","offset":68,"parentId":"500"},
{"id":"507","kind":"UInt64","name":"DeepFreezeStartTime","offset":72,"parentId":"500"},
{"id":"508","kind":"Pointer64","name":"Affinity","offset":80,"parentId":"500"},
{"id":"509","kind":"Hex64","name":"AutoBoostState","offset":88,"parentId":"500"},
{"id":"510","kind":"Struct","name":"ReadyListHead","structTypeName":"_LIST_ENTRY","offset":104,"parentId":"500","refId":"100","collapsed":true},
{"id":"511","kind":"Struct","name":"SwapListEntry","structTypeName":"_SINGLE_LIST_ENTRY","offset":120,"parentId":"500","refId":"110","collapsed":true},
{"id":"512","kind":"Pointer64","name":"ActiveProcessors","offset":128,"parentId":"500"},
{"id":"513","kind":"Struct","name":"","classKeyword":"union","offset":136,"parentId":"500","refId":"0","collapsed":false},
{"id":"514","kind":"Hex32","name":"AutoAlignment:1 DisableBoost:1 DisableQuantum:1 DeepFreeze:1 TimerVirtualization:1 CheckStackExtents:1 CacheIsolationEnabled:1 PpmPolicy:4 VaSpaceDeleted:1 MultiGroup:1 ForegroundProcess:1 ReservedFlags:18","offset":0,"parentId":"513"},
{"id":"515","kind":"Int32","name":"ProcessFlags","offset":0,"parentId":"513"},
{"id":"516","kind":"Int8","name":"BasePriority","offset":144,"parentId":"500"},
{"id":"517","kind":"Int8","name":"QuantumReset","offset":145,"parentId":"500"},
{"id":"518","kind":"Int8","name":"Visited","offset":146,"parentId":"500"},
{"id":"519","kind":"Struct","name":"Flags","structTypeName":"_KEXECUTE_OPTIONS","offset":147,"parentId":"500","refId":"190","collapsed":true},
{"id":"520","kind":"Struct","name":"StackCount","structTypeName":"_KSTACK_COUNT","offset":264,"parentId":"500","refId":"180","collapsed":true},
{"id":"521","kind":"Struct","name":"ProcessListEntry","structTypeName":"_LIST_ENTRY","offset":272,"parentId":"500","refId":"100","collapsed":true},
{"id":"522","kind":"UInt64","name":"CycleTime","offset":288,"parentId":"500"},
{"id":"523","kind":"UInt64","name":"ContextSwitches","offset":296,"parentId":"500"},
{"id":"524","kind":"Pointer64","name":"SchedulingGroup","offset":304,"parentId":"500"},
{"id":"525","kind":"UInt64","name":"KernelTime","offset":312,"parentId":"500"},
{"id":"526","kind":"UInt64","name":"UserTime","offset":320,"parentId":"500"},
{"id":"527","kind":"UInt64","name":"ReadyTime","offset":328,"parentId":"500"},
{"id":"528","kind":"UInt32","name":"FreezeCount","offset":336,"parentId":"500"},
{"id":"529","kind":"UInt64","name":"UserDirectoryTableBase","offset":344,"parentId":"500"},
{"id":"530","kind":"UInt8","name":"AddressPolicy","offset":352,"parentId":"500"},
{"id":"531","kind":"Pointer64","name":"InstrumentationCallback","offset":360,"parentId":"500"},
{"id":"532","kind":"UInt64","name":"SecureHandle","offset":368,"parentId":"500"},
{"id":"533","kind":"UInt64","name":"KernelWaitTime","offset":376,"parentId":"500"},
{"id":"534","kind":"UInt64","name":"UserWaitTime","offset":384,"parentId":"500"},
{"id":"535","kind":"UInt64","name":"LastRebalanceQpc","offset":392,"parentId":"500"},
{"id":"536","kind":"Pointer64","name":"PerProcessorCycleTimes","offset":400,"parentId":"500"},
{"id":"537","kind":"UInt64","name":"ExtendedFeatureDisableMask","offset":408,"parentId":"500"},
{"id":"538","kind":"UInt16","name":"PrimaryGroup","offset":416,"parentId":"500"},
{"id":"539","kind":"Pointer64","name":"UserCetLogging","offset":424,"parentId":"500"},
{"id":"540","kind":"Struct","name":"CpuPartitionList","structTypeName":"_LIST_ENTRY","offset":432,"parentId":"500","refId":"100","collapsed":true},
{"id":"541","kind":"Pointer64","name":"AvailableCpuState","offset":448,"parentId":"500"},
{"id":"2000","kind":"Struct","name":"eprocess","structTypeName":"_EPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":false},
{"id":"2001","kind":"Struct","name":"Pcb","structTypeName":"_KPROCESS","offset":0,"parentId":"2000","refId":"500","collapsed":true},
{"id":"2002","kind":"Struct","name":"ProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":456,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2003","kind":"Pointer64","name":"UniqueProcessId","offset":464,"parentId":"2000"},
{"id":"2004","kind":"Struct","name":"ActiveProcessLinks","structTypeName":"_LIST_ENTRY","offset":472,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2005","kind":"Struct","name":"RundownProtect","structTypeName":"_EX_RUNDOWN_REF","offset":488,"parentId":"2000","refId":"130","collapsed":true},
{"id":"2006","kind":"Struct","name":"","classKeyword":"union","offset":496,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2007","kind":"UInt32","name":"Flags2","offset":0,"parentId":"2006"},
{"id":"2008","kind":"Hex32","name":"JobNotReallyActive:1 AccountingFolded:1 NewProcessReported:1 ExitProcessReported:1 ReportCommitChanges:1 LastReportMemory:1 ForceWakeCharge:1 CrossSessionCreate:1 NeedsHandleRundown:1 RefTraceEnabled:1 PicoCreated:1 EmptyJobEvaluated:1 DefaultPagePriority:3 PrimaryTokenFrozen:1","offset":0,"parentId":"2006"},
{"id":"2009","kind":"Struct","name":"","classKeyword":"union","offset":500,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2010","kind":"UInt32","name":"Flags","offset":0,"parentId":"2009"},
{"id":"2011","kind":"Hex32","name":"CreateReported:1 NoDebugInherit:1 ProcessExiting:1 ProcessDelete:1 ManageExecutableMemoryWrites:1 VmDeleted:1 OutswapEnabled:1 Outswapped:1 FailFastOnCommitFail:1 Wow64VaSpace4Gb:1 AddressSpaceInitialized:2 SetTimerResolution:1 BreakOnTermination:1","offset":0,"parentId":"2009"},
{"id":"2012","kind":"Int64","name":"CreateTime","offset":504,"parentId":"2000"},
{"id":"2013","kind":"UInt64","name":"ProcessQuotaUsage_0","offset":512,"parentId":"2000"},
{"id":"2014","kind":"UInt64","name":"ProcessQuotaUsage_1","offset":520,"parentId":"2000"},
{"id":"2015","kind":"UInt64","name":"ProcessQuotaPeak_0","offset":528,"parentId":"2000"},
{"id":"2016","kind":"UInt64","name":"ProcessQuotaPeak_1","offset":536,"parentId":"2000"},
{"id":"2017","kind":"UInt64","name":"PeakVirtualSize","offset":544,"parentId":"2000"},
{"id":"2018","kind":"UInt64","name":"VirtualSize","offset":552,"parentId":"2000"},
{"id":"2019","kind":"Struct","name":"SessionProcessLinks","structTypeName":"_LIST_ENTRY","offset":560,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2020","kind":"Struct","name":"","classKeyword":"union","offset":576,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2021","kind":"Pointer64","name":"ExceptionPortData","offset":0,"parentId":"2020"},
{"id":"2022","kind":"UInt64","name":"ExceptionPortValue","offset":0,"parentId":"2020"},
{"id":"2023","kind":"Struct","name":"Token","structTypeName":"_EX_FAST_REF","offset":584,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2024","kind":"UInt64","name":"MmReserved","offset":592,"parentId":"2000"},
{"id":"2025","kind":"Struct","name":"AddressCreationLock","structTypeName":"_EX_PUSH_LOCK","offset":600,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2026","kind":"Struct","name":"PageTableCommitmentLock","structTypeName":"_EX_PUSH_LOCK","offset":608,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2027","kind":"Pointer64","name":"RotateInProgress","offset":616,"parentId":"2000"},
{"id":"2028","kind":"Pointer64","name":"ForkInProgress","offset":624,"parentId":"2000"},
{"id":"2029","kind":"Pointer64","name":"CommitChargeJob","offset":632,"parentId":"2000"},
{"id":"2030","kind":"Struct","name":"CloneRoot","structTypeName":"_RTL_AVL_TREE","offset":640,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2031","kind":"UInt64","name":"NumberOfPrivatePages","offset":648,"parentId":"2000"},
{"id":"2032","kind":"UInt64","name":"NumberOfLockedPages","offset":656,"parentId":"2000"},
{"id":"2033","kind":"Pointer64","name":"Win32Process","offset":664,"parentId":"2000"},
{"id":"2034","kind":"Pointer64","name":"Job","offset":672,"parentId":"2000"},
{"id":"2035","kind":"Pointer64","name":"SectionObject","offset":680,"parentId":"2000"},
{"id":"2036","kind":"Pointer64","name":"SectionBaseAddress","offset":688,"parentId":"2000"},
{"id":"2037","kind":"UInt32","name":"Cookie","offset":696,"parentId":"2000"},
{"id":"2038","kind":"Pointer64","name":"WorkingSetWatch","offset":704,"parentId":"2000"},
{"id":"2039","kind":"Pointer64","name":"Win32WindowStation","offset":712,"parentId":"2000"},
{"id":"2040","kind":"Pointer64","name":"InheritedFromUniqueProcessId","offset":720,"parentId":"2000"},
{"id":"2041","kind":"UInt64","name":"OwnerProcessId","offset":728,"parentId":"2000"},
{"id":"2042","kind":"Pointer64","name":"Peb","offset":736,"parentId":"2000"},
{"id":"2043","kind":"Pointer64","name":"Session","offset":744,"parentId":"2000"},
{"id":"2044","kind":"Pointer64","name":"Spare1","offset":752,"parentId":"2000"},
{"id":"2045","kind":"Pointer64","name":"QuotaBlock","offset":760,"parentId":"2000"},
{"id":"2046","kind":"Pointer64","name":"ObjectTable","offset":768,"parentId":"2000"},
{"id":"2047","kind":"Pointer64","name":"DebugPort","offset":776,"parentId":"2000"},
{"id":"2048","kind":"Pointer64","name":"WoW64Process","offset":784,"parentId":"2000"},
{"id":"2049","kind":"Struct","name":"DeviceMap","structTypeName":"_EX_FAST_REF","offset":792,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2050","kind":"Pointer64","name":"EtwDataSource","offset":800,"parentId":"2000"},
{"id":"2051","kind":"UInt64","name":"PageDirectoryPte","offset":808,"parentId":"2000"},
{"id":"2052","kind":"Pointer64","name":"ImageFilePointer","offset":816,"parentId":"2000"},
{"id":"2053","kind":"Hex64","name":"ImageFileName_lo","offset":824,"parentId":"2000"},
{"id":"2054","kind":"Hex32","name":"ImageFileName_mi","offset":832,"parentId":"2000"},
{"id":"2055","kind":"Hex16","name":"ImageFileName_hi","offset":836,"parentId":"2000"},
{"id":"2056","kind":"UInt8","name":"ImageFileName_14","offset":838,"parentId":"2000"},
{"id":"2057","kind":"UInt8","name":"PriorityClass","offset":839,"parentId":"2000"},
{"id":"2058","kind":"Pointer64","name":"SecurityPort","offset":840,"parentId":"2000"},
{"id":"2059","kind":"Struct","name":"SeAuditProcessCreationInfo","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":848,"parentId":"2000","refId":"200","collapsed":true},
{"id":"2060","kind":"Struct","name":"JobLinks","structTypeName":"_LIST_ENTRY","offset":856,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2061","kind":"Pointer64","name":"HighestUserAddress","offset":872,"parentId":"2000"},
{"id":"2062","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":880,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2063","kind":"UInt32","name":"ActiveThreads","offset":896,"parentId":"2000"},
{"id":"2064","kind":"UInt32","name":"ImagePathHash","offset":900,"parentId":"2000"},
{"id":"2065","kind":"UInt32","name":"DefaultHardErrorProcessing","offset":904,"parentId":"2000"},
{"id":"2066","kind":"Int32","name":"LastThreadExitStatus","offset":908,"parentId":"2000"},
{"id":"2067","kind":"Struct","name":"PrefetchTrace","structTypeName":"_EX_FAST_REF","offset":912,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2068","kind":"Pointer64","name":"LockedPagesList","offset":920,"parentId":"2000"},
{"id":"2069","kind":"Int64","name":"ReadOperationCount","offset":928,"parentId":"2000"},
{"id":"2070","kind":"Int64","name":"WriteOperationCount","offset":936,"parentId":"2000"},
{"id":"2071","kind":"Int64","name":"OtherOperationCount","offset":944,"parentId":"2000"},
{"id":"2072","kind":"Int64","name":"ReadTransferCount","offset":952,"parentId":"2000"},
{"id":"2073","kind":"Int64","name":"WriteTransferCount","offset":960,"parentId":"2000"},
{"id":"2074","kind":"Int64","name":"OtherTransferCount","offset":968,"parentId":"2000"},
{"id":"2075","kind":"UInt64","name":"CommitChargeLimit","offset":976,"parentId":"2000"},
{"id":"2076","kind":"UInt64","name":"CommitCharge","offset":984,"parentId":"2000"},
{"id":"2077","kind":"UInt64","name":"CommitChargePeak","offset":992,"parentId":"2000"},
{"id":"2078","kind":"Struct","name":"Vm","structTypeName":"_MMSUPPORT_FULL","offset":1024,"parentId":"2000","refId":"350","collapsed":true},
{"id":"2079","kind":"Struct","name":"MmProcessLinks","structTypeName":"_LIST_ENTRY","offset":1344,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2080","kind":"UInt32","name":"ModifiedPageCount","offset":1360,"parentId":"2000"},
{"id":"2081","kind":"Int32","name":"ExitStatus","offset":1364,"parentId":"2000"},
{"id":"2082","kind":"Struct","name":"VadRoot","structTypeName":"_RTL_AVL_TREE","offset":1368,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2083","kind":"Pointer64","name":"VadHint","offset":1376,"parentId":"2000"},
{"id":"2084","kind":"UInt64","name":"VadCount","offset":1384,"parentId":"2000"},
{"id":"2085","kind":"UInt64","name":"VadPhysicalPages","offset":1392,"parentId":"2000"},
{"id":"2086","kind":"UInt64","name":"VadPhysicalPagesLimit","offset":1400,"parentId":"2000"},
{"id":"2087","kind":"Struct","name":"AlpcContext","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":1408,"parentId":"2000","refId":"250","collapsed":true},
{"id":"2088","kind":"Struct","name":"TimerResolutionLink","structTypeName":"_LIST_ENTRY","offset":1440,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2089","kind":"Pointer64","name":"TimerResolutionStackRecord","offset":1456,"parentId":"2000"},
{"id":"2090","kind":"UInt32","name":"RequestedTimerResolution","offset":1464,"parentId":"2000"},
{"id":"2091","kind":"UInt32","name":"SmallestTimerResolution","offset":1468,"parentId":"2000"},
{"id":"2092","kind":"Int64","name":"ExitTime","offset":1472,"parentId":"2000"},
{"id":"2093","kind":"Pointer64","name":"InvertedFunctionTable","offset":1480,"parentId":"2000"},
{"id":"2094","kind":"Struct","name":"InvertedFunctionTableLock","structTypeName":"_EX_PUSH_LOCK","offset":1488,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2095","kind":"UInt32","name":"ActiveThreadsHighWatermark","offset":1496,"parentId":"2000"},
{"id":"2096","kind":"UInt32","name":"LargePrivateVadCount","offset":1500,"parentId":"2000"},
{"id":"2097","kind":"Struct","name":"ThreadListLock","structTypeName":"_EX_PUSH_LOCK","offset":1504,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2098","kind":"Pointer64","name":"WnfContext","offset":1512,"parentId":"2000"},
{"id":"2099","kind":"Pointer64","name":"ServerSilo","offset":1520,"parentId":"2000"},
{"id":"2100","kind":"UInt8","name":"SignatureLevel","offset":1528,"parentId":"2000"},
{"id":"2101","kind":"UInt8","name":"SectionSignatureLevel","offset":1529,"parentId":"2000"},
{"id":"2102","kind":"Struct","name":"Protection","structTypeName":"_PS_PROTECTION","offset":1530,"parentId":"2000","refId":"210","collapsed":true},
{"id":"2103","kind":"Hex8","name":"HangCount:3 GhostCount:3 PrefilterException:1","offset":1531,"parentId":"2000"},
{"id":"2104","kind":"Struct","name":"","classKeyword":"union","offset":1532,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2105","kind":"UInt32","name":"Flags3","offset":0,"parentId":"2104"},
{"id":"2106","kind":"Hex32","name":"Minimal:1 ReplacingPageRoot:1 Crashed:1 JobVadsAreTracked:1 VadTrackingDisabled:1 AuxiliaryProcess:1 SubsystemProcess:1 IndirectCpuSets:1 RelinquishedCommit:1 HighGraphicsPriority:1 CommitFailLogged:1 ReserveFailLogged:1 SystemProcess:1","offset":0,"parentId":"2104"},
{"id":"2107","kind":"Int32","name":"DeviceAsid","offset":1536,"parentId":"2000"},
{"id":"2108","kind":"Pointer64","name":"SvmData","offset":1544,"parentId":"2000"},
{"id":"2109","kind":"Struct","name":"SvmProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":1552,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2110","kind":"UInt64","name":"SvmLock","offset":1560,"parentId":"2000"},
{"id":"2111","kind":"Struct","name":"SvmProcessDeviceListHead","structTypeName":"_LIST_ENTRY","offset":1568,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2112","kind":"UInt64","name":"LastFreezeInterruptTime","offset":1584,"parentId":"2000"},
{"id":"2113","kind":"Pointer64","name":"DiskCounters","offset":1592,"parentId":"2000"},
{"id":"2114","kind":"Pointer64","name":"PicoContext","offset":1600,"parentId":"2000"},
{"id":"2115","kind":"Pointer64","name":"EnclaveTable","offset":1608,"parentId":"2000"},
{"id":"2116","kind":"UInt64","name":"EnclaveNumber","offset":1616,"parentId":"2000"},
{"id":"2117","kind":"Struct","name":"EnclaveLock","structTypeName":"_EX_PUSH_LOCK","offset":1624,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2118","kind":"UInt32","name":"HighPriorityFaultsAllowed","offset":1632,"parentId":"2000"},
{"id":"2119","kind":"Pointer64","name":"EnergyContext","offset":1640,"parentId":"2000"},
{"id":"2120","kind":"Pointer64","name":"VmContext","offset":1648,"parentId":"2000"},
{"id":"2121","kind":"UInt64","name":"SequenceNumber","offset":1656,"parentId":"2000"},
{"id":"2122","kind":"UInt64","name":"CreateInterruptTime","offset":1664,"parentId":"2000"},
{"id":"2123","kind":"UInt64","name":"CreateUnbiasedInterruptTime","offset":1672,"parentId":"2000"},
{"id":"2124","kind":"UInt64","name":"TotalUnbiasedFrozenTime","offset":1680,"parentId":"2000"},
{"id":"2125","kind":"UInt64","name":"LastAppStateUpdateTime","offset":1688,"parentId":"2000"},
{"id":"2126","kind":"Hex64","name":"LastAppStateUptime:61 LastAppState:3","offset":1696,"parentId":"2000"},
{"id":"2127","kind":"UInt64","name":"SharedCommitCharge","offset":1704,"parentId":"2000"},
{"id":"2128","kind":"Struct","name":"SharedCommitLock","structTypeName":"_EX_PUSH_LOCK","offset":1712,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2129","kind":"Struct","name":"SharedCommitLinks","structTypeName":"_LIST_ENTRY","offset":1720,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2130","kind":"UInt64","name":"AllowedCpuSets","offset":1736,"parentId":"2000"},
{"id":"2131","kind":"UInt64","name":"DefaultCpuSets","offset":1744,"parentId":"2000"},
{"id":"2132","kind":"Pointer64","name":"DiskIoAttribution","offset":1752,"parentId":"2000"},
{"id":"2133","kind":"Pointer64","name":"DxgProcess","offset":1760,"parentId":"2000"},
{"id":"2134","kind":"UInt32","name":"Win32KFilterSet","offset":1768,"parentId":"2000"},
{"id":"2135","kind":"UInt16","name":"Machine","offset":1772,"parentId":"2000"},
{"id":"2136","kind":"UInt8","name":"MmSlabIdentity","offset":1774,"parentId":"2000"},
{"id":"2137","kind":"UInt8","name":"Spare0","offset":1775,"parentId":"2000"},
{"id":"2138","kind":"Struct","name":"ProcessTimerDelay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","offset":1776,"parentId":"2000","refId":"220","collapsed":true},
{"id":"2139","kind":"UInt32","name":"KTimerSets","offset":1784,"parentId":"2000"},
{"id":"2140","kind":"UInt32","name":"KTimer2Sets","offset":1788,"parentId":"2000"},
{"id":"2141","kind":"UInt32","name":"ThreadTimerSets","offset":1792,"parentId":"2000"},
{"id":"2142","kind":"UInt64","name":"VirtualTimerListLock","offset":1800,"parentId":"2000"},
{"id":"2143","kind":"Struct","name":"VirtualTimerListHead","structTypeName":"_LIST_ENTRY","offset":1808,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2144","kind":"Struct","name":"WakeChannel","structTypeName":"_WNF_STATE_NAME","offset":1824,"parentId":"2000","refId":"230","collapsed":true},
{"id":"2145","kind":"Struct","name":"","classKeyword":"union","offset":1872,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2146","kind":"UInt32","name":"MitigationFlags","offset":0,"parentId":"2145"},
{"id":"2147","kind":"Hex32","name":"ControlFlowGuardEnabled:1 ControlFlowGuardExportSuppressionEnabled:1 ControlFlowGuardStrict:1 DisallowStrippedImages:1 ForceRelocateImages:1 HighEntropyASLREnabled:1 StackRandomizationDisabled:1 ExtensionPointDisable:1 DisableDynamicCode:1","offset":0,"parentId":"2145"},
{"id":"2148","kind":"Struct","name":"","classKeyword":"union","offset":1876,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2149","kind":"UInt32","name":"MitigationFlags2","offset":0,"parentId":"2148"},
{"id":"2150","kind":"Hex32","name":"EnableExportAddressFilter:1 AuditExportAddressFilter:1 EnableRopStackPivot:1 AuditRopStackPivot:1 CetUserShadowStacks:1 SpeculativeStoreBypassDisable:1","offset":0,"parentId":"2148"},
{"id":"2151","kind":"Pointer64","name":"PartitionObject","offset":1880,"parentId":"2000"},
{"id":"2152","kind":"UInt64","name":"SecurityDomain","offset":1888,"parentId":"2000"},
{"id":"2153","kind":"UInt64","name":"ParentSecurityDomain","offset":1896,"parentId":"2000"},
{"id":"2154","kind":"Pointer64","name":"CoverageSamplerContext","offset":1904,"parentId":"2000"},
{"id":"2155","kind":"Pointer64","name":"MmHotPatchContext","offset":1912,"parentId":"2000"},
{"id":"2156","kind":"Struct","name":"DynamicEHContinuationTargetsTree","structTypeName":"_RTL_AVL_TREE","offset":1920,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2157","kind":"Struct","name":"DynamicEHContinuationTargetsLock","structTypeName":"_EX_PUSH_LOCK","offset":1928,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2158","kind":"Struct","name":"DynamicEnforcedCetCompatibleRanges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":1936,"parentId":"2000","refId":"240","collapsed":true},
{"id":"2159","kind":"UInt32","name":"DisabledComponentFlags","offset":1952,"parentId":"2000"},
{"id":"2160","kind":"Int32","name":"PageCombineSequence","offset":1956,"parentId":"2000"},
{"id":"2161","kind":"Struct","name":"EnableOptionalXStateFeaturesLock","structTypeName":"_EX_PUSH_LOCK","offset":1960,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2162","kind":"Pointer64","name":"PathRedirectionHashes","offset":1968,"parentId":"2000"},
{"id":"2163","kind":"Pointer64","name":"SyscallProvider","offset":1976,"parentId":"2000"},
{"id":"2164","kind":"Struct","name":"SyscallProviderProcessLinks","structTypeName":"_LIST_ENTRY","offset":1984,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2165","kind":"Hex64","name":"SyscallProviderDispatchContext","offset":2000,"parentId":"2000"},
{"id":"2166","kind":"Struct","name":"","classKeyword":"union","offset":2008,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2167","kind":"UInt32","name":"MitigationFlags3","offset":0,"parentId":"2166"},
{"id":"2168","kind":"Hex32","name":"RestrictCoreSharing:1 DisallowFsctlSystemCalls:1 AuditDisallowFsctlSystemCalls:1 MitigationFlags3Spare:29","offset":0,"parentId":"2166"},
{"id":"2169","kind":"Struct","name":"","classKeyword":"union","offset":2012,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2170","kind":"UInt32","name":"Flags4","offset":0,"parentId":"2169"},
{"id":"2171","kind":"Hex32","name":"ThreadWasActive:1 MinimalTerminate:1 ImageExpansionDisable:1 SessionFirstProcess:1","offset":0,"parentId":"2169"},
{"id":"2172","kind":"Struct","name":"","classKeyword":"union","offset":2016,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2173","kind":"UInt32","name":"SyscallUsage","offset":0,"parentId":"2172"},
{"id":"2174","kind":"Hex32","name":"SystemModuleInformation:1 SystemModuleInformationEx:1 SystemLocksInformation:1 SystemHandleInformation:1 SystemExtendedHandleInformation:1","offset":0,"parentId":"2172"},
{"id":"2175","kind":"Int32","name":"SupervisorDeviceAsid","offset":2020,"parentId":"2000"},
{"id":"2176","kind":"Pointer64","name":"SupervisorSvmData","offset":2024,"parentId":"2000"},
{"id":"2177","kind":"Pointer64","name":"NetworkCounters","offset":2032,"parentId":"2000"},
{"id":"2178","kind":"Hex64","name":"Execution","offset":2040,"parentId":"2000"},
{"id":"2179","kind":"Pointer64","name":"ThreadIndexTable","offset":2048,"parentId":"2000"},
{"id":"2180","kind":"Struct","name":"FreezeWorkLinks","structTypeName":"_LIST_ENTRY","offset":2056,"parentId":"2000","refId":"100","collapsed":true}
]
}

File diff suppressed because it is too large Load Diff

616
src/examples/MMPFN.rcx Normal file
View File

@@ -0,0 +1,616 @@
{
"baseAddress": "FFFFCA8010000000",
"nextId": "3000",
"nodes": [
{
"id": "100",
"kind": "Struct",
"name": "list_entry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "101",
"kind": "Pointer64",
"name": "Flink",
"offset": 0,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "102",
"kind": "Pointer64",
"name": "Blink",
"offset": 8,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "200",
"kind": "Struct",
"name": "balanced_node",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "210",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "211",
"kind": "Pointer64",
"name": "Left",
"offset": 0,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "212",
"kind": "Pointer64",
"name": "Right",
"offset": 8,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "220",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 16,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "221",
"kind": "UInt64",
"name": "ParentValue",
"offset": 0,
"parentId": "220"
},
{
"id": "300",
"kind": "Struct",
"name": "mmpte",
"structTypeName": "_MMPTE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "301",
"kind": "Struct",
"name": "u",
"classKeyword": "union",
"offset": 0,
"parentId": "300",
"refId": "0",
"collapsed": false
},
{
"id": "302",
"kind": "UInt64",
"name": "Long",
"offset": 0,
"parentId": "301"
},
{
"id": "303",
"kind": "Struct",
"name": "Hard",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "301",
"refId": "400",
"collapsed": true
},
{
"id": "304",
"kind": "Struct",
"name": "Proto",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "301",
"refId": "600",
"collapsed": true
},
{
"id": "305",
"kind": "Struct",
"name": "Soft",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "301",
"refId": "500",
"collapsed": true
},
{
"id": "306",
"kind": "Struct",
"name": "Trans",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "301",
"refId": "700",
"collapsed": true
},
{
"id": "307",
"kind": "Struct",
"name": "Subsect",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "301",
"refId": "800",
"collapsed": true
},
{
"id": "308",
"kind": "Struct",
"name": "TimeStamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "301",
"refId": "900",
"collapsed": true
},
{
"id": "309",
"kind": "Struct",
"name": "List",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "301",
"refId": "1000",
"collapsed": true
},
{
"id": "400",
"kind": "Struct",
"name": "mmpte_hardware",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "401",
"kind": "Hex64",
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
"offset": 0,
"parentId": "400"
},
{
"id": "500",
"kind": "Struct",
"name": "mmpte_software",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "501",
"kind": "Hex64",
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
"offset": 0,
"parentId": "500"
},
{
"id": "600",
"kind": "Struct",
"name": "mmpte_prototype",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "601",
"kind": "Hex64",
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
"offset": 0,
"parentId": "600"
},
{
"id": "700",
"kind": "Struct",
"name": "mmpte_transition",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "701",
"kind": "Hex64",
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
"offset": 0,
"parentId": "700"
},
{
"id": "800",
"kind": "Struct",
"name": "mmpte_subsection",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "801",
"kind": "Hex64",
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
"offset": 0,
"parentId": "800"
},
{
"id": "900",
"kind": "Struct",
"name": "mmpte_timestamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "901",
"kind": "Hex64",
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
"offset": 0,
"parentId": "900"
},
{
"id": "1000",
"kind": "Struct",
"name": "mmpte_list",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1001",
"kind": "Hex64",
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
"offset": 0,
"parentId": "1000"
},
{
"id": "1100",
"kind": "Struct",
"name": "mipfnflink",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1101",
"kind": "Hex64",
"name": "Flink",
"offset": 0,
"parentId": "1100"
},
{
"id": "1200",
"kind": "Struct",
"name": "mipfnblink",
"structTypeName": "_MIPFNBLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1201",
"kind": "Hex64",
"name": "Blink",
"offset": 0,
"parentId": "1200"
},
{
"id": "1300",
"kind": "Struct",
"name": "mmpfnentry1",
"structTypeName": "_MMPFNENTRY1",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1301",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1300"
},
{
"id": "1400",
"kind": "Struct",
"name": "mmpfnentry3",
"structTypeName": "_MMPFNENTRY3",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1401",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1400"
},
{
"id": "1500",
"kind": "Struct",
"name": "mi_pfn_flags",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1501",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1500"
},
{
"id": "1600",
"kind": "Struct",
"name": "mi_pfn_flags4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1601",
"kind": "Hex64",
"name": "Flags",
"offset": 0,
"parentId": "1600"
},
{
"id": "1700",
"kind": "Struct",
"name": "mi_pfn_flags5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1701",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1700"
},
{
"id": "2000",
"kind": "Struct",
"name": "mmpfn",
"structTypeName": "_MMPFN",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": false
},
{
"id": "2001",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2010",
"kind": "Struct",
"name": "ListEntry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "2001",
"refId": "100",
"collapsed": true
},
{
"id": "2011",
"kind": "Struct",
"name": "TreeNode",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "2001",
"refId": "200",
"collapsed": true
},
{
"id": "2012",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2001",
"refId": "0",
"collapsed": false
},
{
"id": "2013",
"kind": "Struct",
"name": "u1",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "2012",
"refId": "1100",
"collapsed": true
},
{
"id": "2014",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 8,
"parentId": "2012",
"refId": "0",
"collapsed": false
},
{
"id": "2015",
"kind": "Pointer64",
"name": "PteAddress",
"offset": 0,
"parentId": "2014",
"refId": "300",
"collapsed": true
},
{
"id": "2016",
"kind": "UInt64",
"name": "PteLong",
"offset": 0,
"parentId": "2014"
},
{
"id": "2017",
"kind": "Struct",
"name": "OriginalPte",
"structTypeName": "_MMPTE",
"offset": 16,
"parentId": "2012",
"refId": "300",
"collapsed": true
},
{
"id": "2020",
"kind": "Struct",
"name": "u2",
"structTypeName": "_MIPFNBLINK",
"offset": 24,
"parentId": "2000",
"refId": "1200",
"collapsed": true
},
{
"id": "2030",
"kind": "Struct",
"name": "u3",
"classKeyword": "union",
"offset": 32,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2031",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2030",
"refId": "0",
"collapsed": false
},
{
"id": "2032",
"kind": "UInt16",
"name": "ReferenceCount",
"offset": 0,
"parentId": "2031"
},
{
"id": "2033",
"kind": "Struct",
"name": "e1",
"structTypeName": "_MMPFNENTRY1",
"offset": 2,
"parentId": "2031",
"refId": "1300",
"collapsed": true
},
{
"id": "2034",
"kind": "Struct",
"name": "e4",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "2030",
"refId": "1500",
"collapsed": true
},
{
"id": "2040",
"kind": "Struct",
"name": "u5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 36,
"parentId": "2000",
"refId": "1700",
"collapsed": true
},
{
"id": "2050",
"kind": "Struct",
"name": "u4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 40,
"parentId": "2000",
"refId": "1600",
"collapsed": true
}
]
}

160984
src/examples/Vergilius_25H2.rcx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,344 +0,0 @@
{
"baseAddress": "400000",
"nextId": "29",
"nodes": [
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "1",
"kind": "Struct",
"name": "aBall",
"offset": 0,
"parentId": "0",
"refId": "0",
"strLen": 64,
"structTypeName": "ball"
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "2",
"kind": "Hex64",
"name": "field_00",
"offset": 0,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "3",
"kind": "Hex64",
"name": "field_08",
"offset": 8,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "4",
"kind": "Vec4",
"name": "position",
"offset": 16,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "5",
"kind": "Vec3",
"name": "velocity",
"offset": 32,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "6",
"kind": "Hex32",
"name": "field_2C",
"offset": 44,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "7",
"kind": "Float",
"name": "speed",
"offset": 48,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "8",
"kind": "UInt32",
"name": "color",
"offset": 52,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "9",
"kind": "Float",
"name": "radius",
"offset": 56,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "10",
"kind": "Hex32",
"name": "field_3C",
"offset": 60,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "11",
"kind": "Float",
"name": "mass",
"offset": 64,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "12",
"kind": "Hex64",
"name": "field_44",
"offset": 68,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "13",
"kind": "Bool",
"name": "bouncy",
"offset": 76,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "14",
"kind": "Hex8",
"name": "field_4D",
"offset": 77,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "15",
"kind": "Hex16",
"name": "field_4E",
"offset": 78,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "16",
"kind": "UInt32",
"name": "color",
"offset": 80,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "17",
"kind": "Hex32",
"name": "field_54",
"offset": 84,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "18",
"kind": "Hex64",
"name": "field_58",
"offset": 88,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "19",
"kind": "Hex64",
"name": "field_60",
"offset": 96,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "20",
"kind": "Struct",
"name": "aPhysics",
"offset": 0,
"parentId": "0",
"refId": "0",
"strLen": 64,
"structTypeName": "Physics"
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "21",
"kind": "Hex64",
"name": "field_00",
"offset": 0,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "22",
"kind": "Hex64",
"name": "field_08",
"offset": 8,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "23",
"kind": "Hex64",
"name": "field_10",
"offset": 16,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "24",
"kind": "Hex64",
"name": "field_18",
"offset": 24,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "25",
"kind": "Hex64",
"name": "field_20",
"offset": 32,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": true,
"elementKind": "UInt8",
"id": "26",
"kind": "Pointer64",
"name": "physics",
"offset": 104,
"parentId": "1",
"refId": "20",
"strLen": 64
},
{
"arrayLen": 4,
"collapsed": false,
"elementKind": "Float",
"id": "27",
"kind": "Array",
"name": "scores",
"offset": 112,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 2,
"collapsed": false,
"elementKind": "Struct",
"id": "28",
"kind": "Array",
"name": "materials",
"offset": 128,
"parentId": "1",
"refId": "20",
"strLen": 64
}
]
}

11329
src/examples/t6zm.rcx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
#include "core.h"
#include "addressparser.h"
#include <cmath>
#include <cstring>
#include <limits>
@@ -22,6 +23,14 @@ static QString fit(QString s, int w) {
return s.leftJustified(w, ' ');
}
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
static QString fitOverflow(const QString& s, int w) {
if (w <= 0) return {};
if (s.size() <= w)
return s.leftJustified(w, ' ');
return s;
}
// ── Type name ──
// Override seam: injectable type-name provider
@@ -71,38 +80,38 @@ static QString rawHex(uint64_t v, int digits) {
return QString::number(v, 16).rightJustified(digits, '0');
}
QString fmtInt8(int8_t v) { return hexVal((uint8_t)v); }
QString fmtInt16(int16_t v) { return hexVal((uint16_t)v); }
QString fmtInt32(int32_t v) { return hexVal((uint32_t)v); }
QString fmtInt64(int64_t v) { return hexVal((uint64_t)v); }
QString fmtInt8(int8_t v) { return QString::number(v); }
QString fmtInt16(int16_t v) { return QString::number(v); }
QString fmtInt32(int32_t v) { return QString::number(v); }
QString fmtInt64(int64_t v) { return QString::number((qlonglong)v); }
QString fmtUInt8(uint8_t v) { return hexVal(v); }
QString fmtUInt16(uint16_t v) { return hexVal(v); }
QString fmtUInt32(uint32_t v) { return hexVal(v); }
QString fmtUInt64(uint64_t v) { return hexVal(v); }
QString fmtFloat(float v) {
// Fixed 7-char body: digits + "." + decimals + "f"
// Negative values get a '-' prefix (8 chars total), positive stay 7.
if (std::isnan(v)) return QStringLiteral("NaN");
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
// 6 significant digits — covers full single-precision range
QString s = QString::number(v, 'g', 6);
float av = std::fabs(v);
if (av >= 100000.f)
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
// If 'g' chose scientific notation, reformat as plain decimal
if (s.contains('e') || s.contains('E')) {
s = QString::number(v, 'f', 8);
if (s.contains('.')) {
int i = s.size() - 1;
while (i > 0 && s[i] == '0') i--;
if (s[i] == '.') i++; // keep at least one decimal digit
s.truncate(i + 1);
// body = digits + "." + decimals + "f", target exactly 7 chars.
// Start with max decimals, reduce if integer part is wide or rounding overflows.
for (int dec = 4; dec >= 0; dec--) {
QString body = QString::number(av, 'f', dec);
body += (dec == 0) ? QStringLiteral(".f") : QStringLiteral("f");
if (body.size() == 7) {
if (v < 0.f) body.prepend('-');
return body;
}
}
if (!s.contains('.'))
s += QStringLiteral(".f");
else
s += QLatin1Char('f');
return s;
// Rounding pushed past 99999 — use overflow cap
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
}
QString fmtDouble(double v) {
QString s = QString::number(v, 'g', 6);
@@ -112,15 +121,8 @@ QString fmtDouble(double v) {
}
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
QString fmtPointer32(uint32_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexVal(v);
}
QString fmtPointer64(uint64_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexVal(v);
}
QString fmtPointer32(uint32_t v) { return hexVal(v); }
QString fmtPointer64(uint64_t v) { return hexVal(v); }
// ── Indentation ──
@@ -139,20 +141,25 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
// ── Struct type name (for width calculation) ──
QString structTypeName(const Node& node) {
// Full type string: "struct TypeName" or just "struct" if no typename
QString base = typeName(node.kind).trimmed(); // "struct"
// Named types: just the type name (e.g. "_LIST_ENTRY")
// Anonymous: just the keyword (e.g. "union", "struct")
if (!node.structTypeName.isEmpty())
return base + QStringLiteral(" ") + node.structTypeName;
return base;
return node.structTypeName;
return node.resolvedClassKeyword();
}
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
// Columnar format: <type> <name> { (or no brace when collapsed)
QString ind = indent(depth);
QString type = fit(structTypeName(node), colType);
QString rawType = structTypeName(node);
QString suffix = collapsed ? QString() : QStringLiteral("{");
if (node.name.isEmpty()) {
// Anonymous struct/union: "union {" with no column padding
return ind + rawType + SEP + suffix;
}
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
return ind + type + SEP + node.name + SEP + suffix;
}
@@ -162,9 +169,10 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
// ── Array header ──
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) {
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
QString ind = indent(depth);
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType);
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
QString suffix = collapsed ? QString() : QStringLiteral("{");
return ind + type + SEP + node.name + SEP + suffix;
}
@@ -173,10 +181,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType, int colName) {
const QString& ptrTypeName, int colType, int colName,
bool compact) {
QString ind = indent(depth);
QString type = fit(ptrTypeName, colType);
bool overflow = compact && ptrTypeName.size() > colType;
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
if (collapsed) {
if (overflow) {
// Overflow: no column padding
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
}
// Collapsed: show pointer value instead of brace (name padded for value alignment)
QString name = fit(node.name, colName);
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
@@ -262,15 +276,55 @@ static QString readValueImpl(const Node& node, const Provider& prov,
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Pointer64: {
uint64_t val = prov.readU64(addr);
// Primitive pointer: dereference and show target value
// (hex/ptr/fnptr targets fall through to plain void* display)
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
uint64_t target = val;
for (int d = 1; d < node.ptrDepth && target != 0; d++)
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
// Create a temporary node of the target kind to format the value
Node tmp;
tmp.kind = node.elementKind;
tmp.strLen = node.strLen;
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
if (display) {
QString arrow = QStringLiteral("-> ");
QString sym = prov.getSymbol(val);
if (!sym.isEmpty())
return arrow + derefVal + QStringLiteral(" // ") + sym;
return arrow + derefVal;
}
return derefVal;
}
if (!display) return rawHex(val, 16);
return fmtPointer64(val);
}
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr32: {
uint32_t val = prov.readU32(addr);
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr64: {
uint64_t val = prov.readU64(addr);
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Vec2:
@@ -279,7 +333,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
int count = sizeForKind(node.kind) / 4;
QStringList parts;
for (int i = 0; i < count; i++)
parts << fmtFloat(prov.readF32(addr + i * 4)).trimmed();
parts << fmtFloat(prov.readF32(addr + i * 4));
return parts.join(QStringLiteral(", "));
}
case NodeKind::Mat4x4: {
@@ -288,12 +342,11 @@ static QString readValueImpl(const Node& node, const Provider& prov,
QString line = QStringLiteral("row%1 [").arg(subLine);
for (int c = 0; c < 4; c++) {
if (c > 0) line += QStringLiteral(", ");
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4));
}
line += QStringLiteral("]");
return line;
}
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
case NodeKind::UTF8: {
QByteArray bytes = prov.readBytes(addr, node.strLen);
int end = bytes.indexOf('\0');
@@ -326,12 +379,22 @@ QString readValue(const Node& node, const Provider& prov,
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine,
const QString& comment, int colType, int colName,
const QString& typeOverride) {
const QString& typeOverride, bool compact) {
QString ind = indent(depth);
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
QString name = fit(node.name, colName);
// Compute raw type string for overflow detection
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
bool overflow = compact && rawType.size() > colType;
QString type = overflow ? fitOverflow(rawType, colType)
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
: fit(typeOverride, colType));
QString name = overflow ? node.name : fit(node.name, colName);
// Effective column width for this line (accounts for overflow, clamped to hard max)
int effectiveColType = overflow ? rawType.size() : colType;
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = colType + colName + 2 * kSepWidth;
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
// Comment suffix (only present when a comment is provided; no trailing padding)
QString cmtSuffix = comment.isEmpty() ? QString()
@@ -344,21 +407,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
return ind + QString(prefixW, ' ') + val + cmtSuffix;
}
// Hex nodes and Padding: hex byte preview (ASCII padded to colName to align with value column)
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
if (isHexPreview(node.kind)) {
if (node.kind == NodeKind::Padding) {
const int totalSz = qMax(1, node.arrayLen);
const int lineOff = subLine * 8;
const int lineBytes = qMin(8, totalSz - lineOff);
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
QString ascii = bytesToAscii(b, lineBytes).leftJustified(colName, ' ');
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
if (subLine == 0)
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
}
// Hex8..Hex64: single line, ASCII padded to colName so hex column aligns with value column
const int sz = sizeForKind(node.kind);
QByteArray b = prov.isReadable(addr, sz)
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
@@ -367,7 +417,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
}
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
QString val = overflow ? readValue(node, prov, addr, subLine)
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
return ind + type + SEP + name + SEP + val + cmtSuffix;
}
@@ -557,6 +608,14 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr32: {
uint val = stripHex(s).toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::UTF8: {
*ok = true;
if (s.startsWith('"') && s.endsWith('"'))
@@ -585,7 +644,8 @@ QString validateValue(NodeKind kind, const QString& text) {
// For integer/hex types, validate character set first
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64;
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
if (isHexKind || isIntKind) {
@@ -629,43 +689,41 @@ QString validateValue(NodeKind kind, const QString& text) {
return QStringLiteral("invalid");
}
// ── Base address validation (supports simple +/- equations) ──
// ── Base address validation (delegates to AddressParser) ──
QString validateBaseAddress(const QString& text) {
QString s = text.trimmed();
if (s.isEmpty()) return QStringLiteral("empty");
//s.remove('`');
return AddressParser::validate(s);
}
int pos = 0;
bool firstTerm = true;
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
QString ind = indent(depth);
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
}
while (pos < s.size()) {
// Skip whitespace
while (pos < s.size() && s[pos].isSpace()) pos++;
if (pos >= s.size()) break;
// ── Bitfield member formatting ──
// Check for +/- operator (except first term)
if (!firstTerm) {
if (s[pos] == '+' || s[pos] == '-') pos++;
else return QStringLiteral("invalid '%1'").arg(s[pos]);
while (pos < s.size() && s[pos].isSpace()) pos++;
}
// Skip 0x prefix if present
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
pos += 2;
// Must have at least one hex digit
int numStart = pos;
while (pos < s.size() && (s[pos].isDigit() ||
(s[pos] >= 'a' && s[pos] <= 'f') ||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
if (pos == numStart) return QStringLiteral("invalid");
firstTerm = false;
uint64_t extractBits(const Provider& prov, uint64_t addr,
NodeKind containerKind,
uint8_t bitOffset, uint8_t bitWidth) {
uint64_t container = 0;
switch (containerKind) {
case NodeKind::Hex8: container = prov.readU8(addr); break;
case NodeKind::Hex16: container = prov.readU16(addr); break;
case NodeKind::Hex32: container = prov.readU32(addr); break;
default: container = prov.readU64(addr); break;
}
if (bitWidth >= 64) return container >> bitOffset;
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
}
return {};
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
uint64_t value, int depth, int nameW) {
QString ind = indent(depth);
return ind + name.leftJustified(nameW)
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
}
} // namespace rcx::fmt

View File

@@ -44,13 +44,14 @@ static QString cTypeName(NodeKind kind) {
case NodeKind::Bool: return QStringLiteral("bool");
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
case NodeKind::FuncPtr32: return QStringLiteral("uint32_t");
case NodeKind::FuncPtr64: return QStringLiteral("uint64_t");
case NodeKind::Vec2: return QStringLiteral("float");
case NodeKind::Vec3: return QStringLiteral("float");
case NodeKind::Vec4: return QStringLiteral("float");
case NodeKind::Mat4x4: return QStringLiteral("float");
case NodeKind::UTF8: return QStringLiteral("char");
case NodeKind::UTF16: return QStringLiteral("wchar_t");
case NodeKind::Padding: return QStringLiteral("uint8_t");
default: return QStringLiteral("uint8_t");
}
}
@@ -67,6 +68,7 @@ struct GenContext {
QString output;
int padCounter = 0;
const QHash<NodeKind, QString>* typeAliases = nullptr;
bool emitAsserts = false;
QString uniquePadName() {
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
@@ -99,80 +101,93 @@ static void emitStruct(GenContext& ctx, uint64_t structId);
static const QChar kCommentMarker = QChar(0x01);
static QString offsetComment(int offset) {
static QString offsetComment(int offset, bool isSizeof = false) {
if (isSizeof)
return QString(kCommentMarker) + QStringLiteral("// sizeof 0x%1").arg(QString::number(offset, 16).toUpper());
return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper());
}
static QString emitField(GenContext& ctx, const Node& node) {
static QString indent(int depth) {
return QString(depth * 4, ' ');
}
static QString emitField(GenContext& ctx, const Node& node, int depth, int baseOffset) {
const NodeTree& tree = ctx.tree;
QString ind = indent(depth);
QString name = sanitizeIdent(node.name.isEmpty()
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
: node.name);
QString oc = offsetComment(node.offset);
QString oc = offsetComment(baseOffset + node.offset);
switch (node.kind) {
case NodeKind::Vec2:
return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
return ind + QStringLiteral("%1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Vec3:
return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
return ind + QStringLiteral("%1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Vec4:
return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
return ind + QStringLiteral("%1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::Mat4x4:
return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
return ind + QStringLiteral("%1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
case NodeKind::UTF8:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
case NodeKind::UTF16:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
case NodeKind::Padding:
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc;
case NodeKind::Pointer32: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) +
offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target));
}
}
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
}
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
case NodeKind::Pointer32:
case NodeKind::Pointer64: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" %1* %2;").arg(target, name) + oc;
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
}
}
return QStringLiteral(" void* %1;").arg(name) + oc;
// Native pointer: use void* when this is the target's natural pointer kind
bool isNativePtr = (node.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4)
|| (node.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8);
if (isNativePtr)
return ind + QStringLiteral("void* %1;").arg(name) + oc;
// Cross-size pointer: fall back to raw integer type
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
}
case NodeKind::FuncPtr32:
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
case NodeKind::FuncPtr64:
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
default:
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
}
}
// ── Emit struct body (fields + padding) ──
// ── Emit struct body (fields + padding) — Vergilius-style ──
static void emitStructBody(GenContext& ctx, uint64_t structId) {
static void emitStructBody(GenContext& ctx, uint64_t structId,
bool isUnion, int depth, int baseOffset) {
const NodeTree& tree = ctx.tree;
int idx = tree.indexOfId(structId);
if (idx < 0) return;
int structSize = tree.structSpan(structId, &ctx.childMap);
QString ind = indent(depth);
QVector<int> children = ctx.childMap.value(structId);
QVector<int> allChildren = ctx.childMap.value(structId);
QVector<int> children, staticIdxs;
for (int ci : allChildren) {
if (tree.nodes[ci].isStatic)
staticIdxs.append(ci);
else
children.append(ci);
}
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
// Helper: emit a padding/hex run as a single collapsed byte array
auto emitPadRun = [&](int offset, int size) {
auto emitPadRun = [&](int relOffset, int size) {
if (size <= 0) return;
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
.arg(ctx.cType(NodeKind::Padding))
ctx.output += ind + QStringLiteral("uint8_t %1[0x%2];%3\n")
.arg(ctx.uniquePadName())
.arg(QString::number(size, 16).toUpper())
.arg(offsetComment(offset));
.arg(offsetComment(baseOffset + relOffset));
};
int cursor = 0;
@@ -186,13 +201,15 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
else
childSize = child.byteSize();
// Gap before this field
if (child.offset > cursor)
emitPadRun(cursor, child.offset - cursor);
else if (child.offset < cursor)
ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
.arg(QString::number(child.offset, 16).toUpper())
.arg(QString::number(cursor, 16).toUpper());
// Gap/overlap handling (skip for unions)
if (!isUnion) {
if (child.offset > cursor)
emitPadRun(cursor, child.offset - cursor);
else if (child.offset < cursor)
ctx.output += ind + QStringLiteral("// WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
.arg(QString::number(baseOffset + child.offset, 16).toUpper())
.arg(QString::number(baseOffset + cursor, 16).toUpper());
}
// Collapse consecutive hex nodes into a single padding array
if (isHexNode(child.kind)) {
@@ -203,8 +220,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
int nextSize = next.byteSize();
// Allow gaps within the run (they become part of the pad)
if (next.offset < runEnd) break; // overlap — stop merging
if (next.offset < runEnd) break;
runEnd = next.offset + nextSize;
j++;
}
@@ -216,10 +232,53 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
// Emit the field
if (child.kind == NodeKind::Struct) {
emitStruct(ctx, child.id);
QString typeName = ctx.structName(child);
QString fieldName = sanitizeIdent(child.name);
ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset));
// Bitfield container — emit inline bitfield members
if (child.classKeyword == QStringLiteral("bitfield")
&& !child.bitfieldMembers.isEmpty()) {
QString bfType = ctx.cType(child.elementKind);
if (bfType.isEmpty()) bfType = QStringLiteral("uint32_t");
QString fieldName = child.name.isEmpty()
? QString() : QStringLiteral(" ") + sanitizeIdent(child.name);
ctx.output += ind + QStringLiteral("struct\n");
ctx.output += ind + QStringLiteral("{\n");
QString bfInd = indent(depth + 1);
for (const auto& m : child.bitfieldMembers) {
ctx.output += bfInd + bfType + QStringLiteral(" ")
+ sanitizeIdent(m.name) + QStringLiteral(" : ")
+ QString::number(m.bitWidth) + QStringLiteral(";")
+ offsetComment(baseOffset + child.offset)
+ QStringLiteral("\n");
}
ctx.output += ind + QStringLiteral("}") + fieldName + QStringLiteral(";")
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
} else {
bool isAnonymous = child.structTypeName.isEmpty();
if (isAnonymous) {
// Inline anonymous struct/union
QString kw = child.resolvedClassKeyword();
ctx.output += ind + kw + QStringLiteral("\n");
ctx.output += ind + QStringLiteral("{\n");
bool childIsUnion = (kw == QStringLiteral("union"));
emitStructBody(ctx, child.id, childIsUnion, depth + 1,
baseOffset + child.offset);
QString fieldName = child.name.isEmpty()
? QString() : QStringLiteral(" ") + sanitizeIdent(child.name);
ctx.output += ind + QStringLiteral("}") + fieldName + QStringLiteral(";")
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
} else {
// Named struct — reference by name with struct keyword prefix
QString kw = child.resolvedClassKeyword();
if (kw == QStringLiteral("enum") && child.enumMembers.isEmpty())
kw = QStringLiteral("struct");
QString typeName = sanitizeIdent(child.structTypeName);
QString fieldName = sanitizeIdent(child.name);
ctx.output += ind + kw + QStringLiteral(" ") + typeName
+ QStringLiteral(" ") + fieldName + QStringLiteral(";")
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
}
} // end bitfield else
} else if (child.kind == NodeKind::Array) {
QVector<int> arrayKids = ctx.childMap.value(child.id);
bool hasStructChild = false;
@@ -228,7 +287,6 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
for (int ak : arrayKids) {
if (tree.nodes[ak].kind == NodeKind::Struct) {
hasStructChild = true;
emitStruct(ctx, tree.nodes[ak].id);
elemTypeName = ctx.structName(tree.nodes[ak]);
break;
}
@@ -236,14 +294,16 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
QString fieldName = sanitizeIdent(child.name);
if (hasStructChild && !elemTypeName.isEmpty()) {
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
ctx.output += ind + QStringLiteral("struct %1 %2[%3];%4\n")
.arg(elemTypeName, fieldName).arg(child.arrayLen)
.arg(offsetComment(baseOffset + child.offset));
} else {
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
ctx.output += ind + QStringLiteral("%1 %2[%3];%4\n")
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen)
.arg(offsetComment(baseOffset + child.offset));
}
} else {
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
ctx.output += emitField(ctx, child, depth, baseOffset) + QStringLiteral("\n");
}
int childEnd = child.offset + childSize;
@@ -251,12 +311,20 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
i++;
}
// Tail padding
if (cursor < structSize)
// Tail padding (skip for unions)
if (!isUnion && cursor < structSize)
emitPadRun(cursor, structSize - cursor);
// Emit static field comments (static fields are runtime-only, not part of struct layout)
for (int si : staticIdxs) {
const Node& sf = tree.nodes[si];
QString sfType = sf.structTypeName.isEmpty() ? ctx.cType(sf.kind) : sf.structTypeName;
ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n")
.arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr);
}
}
// ── Emit a complete struct definition ──
// ── Emit a complete top-level struct definition (Vergilius-style) ──
static void emitStruct(GenContext& ctx, uint64_t structId) {
if (ctx.emittedIds.contains(structId)) return;
@@ -272,19 +340,12 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
return;
}
// For arrays, we don't emit a top-level struct — the array itself
// is a field inside its parent. But we do emit struct element types.
if (node.kind == NodeKind::Array) {
QVector<int> kids = ctx.childMap.value(structId);
for (int ki : kids) {
if (ctx.tree.nodes[ki].kind == NodeKind::Struct)
emitStruct(ctx, ctx.tree.nodes[ki].id);
}
ctx.visiting.remove(structId);
return;
}
// Deduplicate by struct type name (different nodes may share the same type)
// Deduplicate by struct type name
QString typeName = ctx.structName(node);
if (ctx.emittedTypeNames.contains(typeName)) {
ctx.emittedIds.insert(structId);
@@ -292,47 +353,39 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
return;
}
// Emit nested struct types first (dependency order)
QVector<int> children = ctx.childMap.value(structId);
for (int ci : children) {
const Node& child = ctx.tree.nodes[ci];
if (child.kind == NodeKind::Struct)
emitStruct(ctx, child.id);
else if (child.kind == NodeKind::Array) {
QVector<int> arrayKids = ctx.childMap.value(child.id);
for (int ak : arrayKids) {
if (ctx.tree.nodes[ak].kind == NodeKind::Struct)
emitStruct(ctx, ctx.tree.nodes[ak].id);
}
}
// Forward-declare pointer target types if they're outside this subtree
if (child.kind == NodeKind::Pointer64 && child.refId != 0) {
int refIdx = ctx.tree.indexOfId(child.refId);
if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId)
&& !ctx.forwardDeclared.contains(child.refId)) {
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
ctx.forwardDeclared.insert(child.refId);
}
}
}
ctx.emittedIds.insert(structId);
ctx.emittedTypeNames.insert(typeName);
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
QString kw = node.resolvedClassKeyword();
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
emitStructBody(ctx, structId);
// Enum with members: emit as proper C enum
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
for (const auto& m : node.enumMembers) {
ctx.output += QStringLiteral(" %1 = %2,\n")
.arg(sanitizeIdent(m.first))
.arg(m.second);
}
ctx.output += QStringLiteral("};\n\n");
ctx.visiting.remove(structId);
return;
}
ctx.output += QStringLiteral("};\n");
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
.arg(typeName)
.arg(QString::number(structSize, 16).toUpper());
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct");
ctx.output += kw + QStringLiteral(" ") + typeName + QStringLiteral("\n{\n");
emitStructBody(ctx, structId, kw == QStringLiteral("union"), 1, 0);
ctx.output += QStringLiteral("};")
+ offsetComment(structSize, true)
+ QStringLiteral("\n");
if (ctx.emitAsserts)
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n")
.arg(typeName)
.arg(QString::number(structSize, 16).toUpper());
ctx.output += QStringLiteral("\n");
ctx.visiting.remove(structId);
}
@@ -386,14 +439,15 @@ static QString alignComments(const QString& raw) {
// ── Public API ──
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases) {
const QHash<NodeKind, QString>* typeAliases,
bool emitAsserts) {
int idx = tree.indexOfId(rootStructId);
if (idx < 0) return {};
const Node& root = tree.nodes[idx];
if (root.kind != NodeKind::Struct) return {};
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
ctx.output += QStringLiteral("#pragma once\n\n");
@@ -403,8 +457,9 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
}
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
const QHash<NodeKind, QString>* typeAliases,
bool emitAsserts) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
ctx.output += QStringLiteral("#pragma once\n\n");

View File

@@ -9,11 +9,13 @@ namespace rcx {
// Generate C++ struct definitions for a single root struct and all
// nested/referenced types reachable from it.
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr);
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// Generate C++ struct definitions for every root-level struct (full SDK).
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr);
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// Null generator placeholder (returns empty string).
QString renderNull(const NodeTree& tree, uint64_t rootStructId);

BIN
src/icons/class.icns Normal file

Binary file not shown.

View File

@@ -0,0 +1,222 @@
#include "export_reclass_xml.h"
#include <QFile>
#include <QXmlStreamWriter>
#include <QHash>
#include <QVector>
#include <algorithm>
namespace rcx {
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
static int xmlTypeForKind(NodeKind kind) {
switch (kind) {
case NodeKind::Struct: return 1; // ClassInstance
case NodeKind::Hex32: return 4;
case NodeKind::Hex64: return 5;
case NodeKind::Hex16: return 6;
case NodeKind::Hex8: return 7;
case NodeKind::Pointer64: return 8; // ClassPointer
case NodeKind::Pointer32: return 8;
case NodeKind::Int64: return 9;
case NodeKind::Int32: return 10;
case NodeKind::Int16: return 11;
case NodeKind::Int8: return 12;
case NodeKind::Float: return 13;
case NodeKind::Double: return 14;
case NodeKind::UInt32: return 15;
case NodeKind::UInt16: return 16;
case NodeKind::UInt8: return 17;
case NodeKind::UInt64: return 32;
case NodeKind::UTF8: return 18;
case NodeKind::UTF16: return 19;
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
case NodeKind::Vec2: return 22;
case NodeKind::Vec3: return 23;
case NodeKind::Vec4: return 24;
case NodeKind::Mat4x4: return 25;
case NodeKind::Array: return 27; // ClassInstanceArray
}
return 7; // fallback to Hex8
}
static int nodeSizeForExport(const Node& node) {
switch (node.kind) {
case NodeKind::UTF8: return node.strLen;
case NodeKind::UTF16: return node.strLen * 2;
case NodeKind::Array: {
int elemSz = sizeForKind(node.elementKind);
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
}
default: return sizeForKind(node.kind);
}
}
// Resolve a struct type name from a node ID
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
int idx = tree.indexOfId(refId);
if (idx < 0) return {};
const Node& ref = tree.nodes[idx];
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
return ref.name;
}
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
if (tree.nodes.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
return false;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
return false;
}
// Build child map
QHash<uint64_t, QVector<int>> childMap;
for (int i = 0; i < tree.nodes.size(); i++)
childMap[tree.nodes[i].parentId].append(i);
QXmlStreamWriter xml(&file);
xml.setAutoFormatting(true);
xml.setAutoFormattingIndent(4);
xml.writeStartDocument();
xml.writeStartElement(QStringLiteral("ReClass"));
xml.writeComment(QStringLiteral("ReClassEx"));
// Get root structs
QVector<int> roots = childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int classCount = 0;
for (int ri : roots) {
const Node& root = tree.nodes[ri];
if (root.kind != NodeKind::Struct) continue;
xml.writeStartElement(QStringLiteral("Class"));
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("Code"), QString());
// Get children sorted by offset
QVector<int> children = childMap.value(root.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int i = 0;
while (i < children.size()) {
const Node& child = tree.nodes[children[i]];
// Bitfield container: export as hex node (ReClassEx has no bitfield concept)
if (child.kind == NodeKind::Struct
&& child.resolvedClassKeyword() == QStringLiteral("bitfield")) {
int sz = child.byteSize();
if (sz <= 0) sz = 4;
xml.writeStartElement(QStringLiteral("Node"));
xml.writeAttribute(QStringLiteral("Name"), child.name);
NodeKind hexKind = (sz <= 1) ? NodeKind::Hex8 : (sz <= 2) ? NodeKind::Hex16
: (sz <= 4) ? NodeKind::Hex32 : NodeKind::Hex64;
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(hexKind)));
xml.writeAttribute(QStringLiteral("Size"), QString::number(sz));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QStringLiteral("bitfield"));
xml.writeEndElement();
i++;
continue;
}
// Collapse consecutive hex nodes into a single Custom node (Type=21)
if (isHexNode(child.kind)) {
int runStart = child.offset;
int runEnd = child.offset + child.byteSize();
int j = i + 1;
while (j < children.size()) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
if (next.offset < runEnd) break; // overlap
runEnd = next.offset + next.byteSize();
j++;
}
int totalSize = runEnd - runStart;
xml.writeStartElement(QStringLiteral("Node"));
// Use first hex node's name if it's a single node, otherwise generate
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
xml.writeAttribute(QStringLiteral("Name"), hexName);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeEndElement(); // Node
i = j;
continue;
}
xml.writeStartElement(QStringLiteral("Node"));
xml.writeAttribute(QStringLiteral("Name"), child.name);
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
// Pointer with target
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
QString target = resolveStructName(tree, child.refId);
if (!target.isEmpty())
xml.writeAttribute(QStringLiteral("Pointer"), target);
}
// Embedded struct instance
if (child.kind == NodeKind::Struct) {
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
xml.writeAttribute(QStringLiteral("Instance"), instName);
}
// Array: Total attribute and child <Array> element
if (child.kind == NodeKind::Array) {
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
// Resolve element type name
QString elemName;
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
elemName = child.structTypeName;
} else if (child.refId != 0) {
elemName = resolveStructName(tree, child.refId);
}
if (elemName.isEmpty())
elemName = kindToString(child.elementKind);
xml.writeStartElement(QStringLiteral("Array"));
xml.writeAttribute(QStringLiteral("Name"), elemName);
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
xml.writeEndElement(); // Array
}
xml.writeEndElement(); // Node
i++;
}
xml.writeEndElement(); // Class
classCount++;
}
xml.writeEndElement(); // ReClass
xml.writeEndDocument();
file.close();
if (classCount == 0) {
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
return false;
}
return true;
}
} // namespace rcx

View File

@@ -0,0 +1,10 @@
#pragma once
#include "core.h"
namespace rcx {
// Export a NodeTree to ReClass .NET / ReClassEx compatible XML format.
// Returns true on success; populates errorMsg on failure if non-null.
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg = nullptr);
} // namespace rcx

1147
src/imports/import_pdb.cpp Normal file

File diff suppressed because it is too large Load Diff

35
src/imports/import_pdb.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "core.h"
#include <QVector>
#include <functional>
namespace rcx {
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union/enum name
uint64_t size; // sizeof in bytes
int childCount; // direct member count
bool isUnion; // union vs struct/class
bool isEnum = false; // enum type
};
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
QString* errorMsg = nullptr);
// Phase 2: Import selected types with full recursive child types.
// progressCb is called with (current, total) for each top-level type;
// return false from the callback to cancel the import.
using ProgressCb = std::function<bool(int current, int total)>;
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg = nullptr,
ProgressCb progressCb = {});
// Legacy single-call API: import one struct by name (or all if filter empty).
NodeTree importPdb(const QString& pdbPath,
const QString& structFilter = {},
QString* errorMsg = nullptr);
} // namespace rcx

View File

@@ -0,0 +1,184 @@
#include "import_pdb_dialog.h"
#include "import_pdb.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QCheckBox>
#include <QListWidget>
#include <QLabel>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QFileDialog>
#include <QMessageBox>
#include <QApplication>
namespace rcx {
PdbImportDialog::PdbImportDialog(QWidget* parent)
: QDialog(parent)
{
setWindowTitle("Import from PDB");
resize(520, 480);
auto* layout = new QVBoxLayout(this);
// PDB path row
auto* pathRow = new QHBoxLayout;
pathRow->addWidget(new QLabel("PDB File:"));
m_pathEdit = new QLineEdit;
m_pathEdit->setPlaceholderText("Select a PDB file...");
pathRow->addWidget(m_pathEdit);
m_browseBtn = new QPushButton("...");
m_browseBtn->setFixedWidth(32);
pathRow->addWidget(m_browseBtn);
layout->addLayout(pathRow);
// Filter row
auto* filterRow = new QHBoxLayout;
filterRow->addWidget(new QLabel("Filter:"));
m_filterEdit = new QLineEdit;
m_filterEdit->setPlaceholderText("Type name filter...");
m_filterEdit->setEnabled(false);
filterRow->addWidget(m_filterEdit);
layout->addLayout(filterRow);
// Select all checkbox
m_selectAll = new QCheckBox("Select All");
m_selectAll->setEnabled(false);
layout->addWidget(m_selectAll);
// Type list
m_typeList = new QListWidget;
m_typeList->setEnabled(false);
layout->addWidget(m_typeList);
// Count label
m_countLabel = new QLabel("No PDB loaded");
layout->addWidget(m_countLabel);
// Buttons
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_buttons->button(QDialogButtonBox::Ok)->setText("Import");
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
layout->addWidget(m_buttons);
connect(m_browseBtn, &QPushButton::clicked, this, &PdbImportDialog::browsePdb);
connect(m_pathEdit, &QLineEdit::returnPressed, this, &PdbImportDialog::loadPdb);
connect(m_filterEdit, &QLineEdit::textChanged, this, &PdbImportDialog::filterChanged);
connect(m_selectAll, &QCheckBox::toggled, this, &PdbImportDialog::selectAllToggled);
connect(m_typeList, &QListWidget::itemChanged, this, &PdbImportDialog::updateSelectionCount);
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QString PdbImportDialog::pdbPath() const {
return m_pathEdit->text();
}
QVector<uint32_t> PdbImportDialog::selectedTypeIndices() const {
QVector<uint32_t> result;
for (int i = 0; i < m_typeList->count(); i++) {
auto* item = m_typeList->item(i);
if (item->checkState() == Qt::Checked) {
uint32_t typeIndex = item->data(Qt::UserRole).toUInt();
result.append(typeIndex);
}
}
return result;
}
void PdbImportDialog::browsePdb() {
QString path = QFileDialog::getOpenFileName(this,
"Select PDB File", {},
"PDB Files (*.pdb);;All Files (*)");
if (path.isEmpty()) return;
m_pathEdit->setText(path);
loadPdb();
}
void PdbImportDialog::loadPdb() {
QString path = m_pathEdit->text();
if (path.isEmpty()) return;
m_typeList->clear();
m_allTypes.clear();
m_countLabel->setText("Loading...");
m_typeList->setEnabled(false);
m_filterEdit->setEnabled(false);
m_selectAll->setEnabled(false);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
QApplication::processEvents();
QString error;
QVector<PdbTypeInfo> types = enumeratePdbTypes(path, &error);
if (types.isEmpty()) {
m_countLabel->setText(error.isEmpty() ? "No types found" : error);
return;
}
m_allTypes.reserve(types.size());
for (const auto& t : types) {
TypeItem item;
item.typeIndex = t.typeIndex;
item.name = t.name;
item.childCount = t.childCount;
item.isUnion = t.isUnion;
m_allTypes.append(item);
}
// Sort by name
std::sort(m_allTypes.begin(), m_allTypes.end(),
[](const TypeItem& a, const TypeItem& b) { return a.name < b.name; });
m_filterEdit->setEnabled(true);
m_selectAll->setEnabled(true);
m_typeList->setEnabled(true);
populateList();
}
void PdbImportDialog::populateList() {
m_typeList->blockSignals(true);
m_typeList->clear();
QString filter = m_filterEdit->text();
bool selectAll = m_selectAll->isChecked();
for (const auto& t : m_allTypes) {
if (!filter.isEmpty() && !t.name.contains(filter, Qt::CaseInsensitive))
continue;
QString label = QStringLiteral("%1 (%2 fields)")
.arg(t.name).arg(t.childCount);
auto* item = new QListWidgetItem(label, m_typeList);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(selectAll ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, t.typeIndex);
}
m_typeList->blockSignals(false);
updateSelectionCount();
}
void PdbImportDialog::filterChanged(const QString&) {
populateList();
}
void PdbImportDialog::selectAllToggled(bool) {
populateList();
}
void PdbImportDialog::updateSelectionCount() {
int checked = 0;
int total = m_typeList->count();
for (int i = 0; i < total; i++) {
if (m_typeList->item(i)->checkState() == Qt::Checked)
checked++;
}
m_countLabel->setText(QStringLiteral("%1 of %2 types selected")
.arg(checked).arg(m_allTypes.size()));
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(checked > 0);
}
} // namespace rcx

View File

@@ -0,0 +1,53 @@
#pragma once
#include <QDialog>
#include <QVector>
#include <cstdint>
class QLineEdit;
class QCheckBox;
class QListWidget;
class QLabel;
class QDialogButtonBox;
class QPushButton;
namespace rcx {
struct PdbTypeInfo;
class PdbImportDialog : public QDialog {
Q_OBJECT
public:
explicit PdbImportDialog(QWidget* parent = nullptr);
QString pdbPath() const;
QVector<uint32_t> selectedTypeIndices() const;
private slots:
void browsePdb();
void loadPdb();
void filterChanged(const QString& text);
void selectAllToggled(bool checked);
void updateSelectionCount();
private:
QLineEdit* m_pathEdit;
QPushButton* m_browseBtn;
QLineEdit* m_filterEdit;
QCheckBox* m_selectAll;
QListWidget* m_typeList;
QLabel* m_countLabel;
QDialogButtonBox* m_buttons;
struct TypeItem {
uint32_t typeIndex;
QString name;
int childCount;
bool isUnion;
};
QVector<TypeItem> m_allTypes;
void populateList();
};
} // namespace rcx

View File

@@ -0,0 +1,392 @@
#include "import_reclass_xml.h"
#include <QFile>
#include <QXmlStreamReader>
#include <QHash>
#include <QVector>
#include <QDebug>
namespace rcx {
// ── Version-specific type maps ──
// Maps XML Type attribute (integer) → NodeKind.
// Entries with no rcx equivalent use Hex8 as fallback.
enum class XmlVersion { V2013, V2016 };
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
// 0: null (unused)
{ 1, NodeKind::Struct }, // ClassInstance
// 2,3: null
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex64 },
{ 6, NodeKind::Hex16 },
{ 7, NodeKind::Hex8 },
{ 8, NodeKind::Pointer64 }, // ClassPointer
{ 9, NodeKind::Int64 },
{ 10, NodeKind::Int32 },
{ 11, NodeKind::Int16 },
{ 12, NodeKind::Int8 },
{ 13, NodeKind::Float },
{ 14, NodeKind::Double },
{ 15, NodeKind::UInt32 },
{ 16, NodeKind::UInt16 },
{ 17, NodeKind::UInt8 },
{ 18, NodeKind::UTF8 }, // UTF8Text
{ 19, NodeKind::UTF16 }, // UTF16Text
{ 20, NodeKind::Pointer64 }, // FunctionPtr
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
{ 22, NodeKind::Vec2 },
{ 23, NodeKind::Vec3 },
{ 24, NodeKind::Vec4 },
{ 25, NodeKind::Mat4x4 },
{ 26, NodeKind::Pointer64 }, // VTable
{ 27, NodeKind::Array }, // ClassInstanceArray
// 28: null (used for Class elements, not nodes)
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
// 31: BitField → UInt8 fallback
{ 31, NodeKind::UInt8 },
{ 32, NodeKind::UInt64 },
{ 33, NodeKind::Pointer64 }, // Function
};
// 2013 / ReClass 2011 type map (31 entries)
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
{ 1, NodeKind::Struct }, // ClassInstance
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex16 },
{ 6, NodeKind::Hex8 },
{ 7, NodeKind::Pointer64 }, // ClassPointer
{ 8, NodeKind::Int32 },
{ 9, NodeKind::Int16 },
{ 10, NodeKind::Int8 },
{ 11, NodeKind::Float },
{ 12, NodeKind::UInt32 },
{ 13, NodeKind::UInt16 },
{ 14, NodeKind::UInt8 },
{ 15, NodeKind::UTF8 }, // UTF8Text
{ 16, NodeKind::Pointer64 }, // FunctionPtr
{ 17, NodeKind::Hex8 }, // Custom
{ 18, NodeKind::Vec2 },
{ 19, NodeKind::Vec3 },
{ 20, NodeKind::Vec4 },
{ 21, NodeKind::Mat4x4 },
{ 22, NodeKind::Pointer64 }, // VTable
{ 23, NodeKind::Array }, // ClassInstanceArray
{ 27, NodeKind::Int64 },
{ 28, NodeKind::Double },
{ 29, NodeKind::UTF16 }, // UTF16Text
{ 30, NodeKind::Array }, // ClassPointerArray
};
static NodeKind lookupKind(int xmlType, XmlVersion ver, int ptrSize = 8) {
NodeKind k = NodeKind::Hex8;
if (ver == XmlVersion::V2016) {
for (const auto& e : kTypeMap2016)
if (e.xmlType == xmlType) { k = e.kind; break; }
} else {
for (const auto& e : kTypeMap2013)
if (e.xmlType == xmlType) { k = e.kind; break; }
}
// Remap pointer types for 32-bit targets
if (ptrSize < 8 && k == NodeKind::Pointer64)
k = NodeKind::Pointer32;
return k;
}
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
static bool isPointerType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016)
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
else
return xmlType == 7 || xmlType == 16 || xmlType == 22;
}
// Is this XML type a ClassInstance (embedded struct)?
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 1;
else return xmlType == 1;
}
// Is this XML type a ClassInstanceArray?
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 27;
else return xmlType == 23 || xmlType == 30;
}
// Is this XML type a text node?
static bool isTextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
else return xmlType == 15 || xmlType == 29;
}
// Is this XML type a UTF16 text node?
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 19;
else return xmlType == 29;
}
// Is this XML type a Custom node (expanded to hex)?
static bool isCustomType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 21;
else return xmlType == 17;
}
// Deferred pointer resolution entry
struct PendingRef {
uint64_t nodeId;
QString className;
};
NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointerSize) {
qDebug() << "[ImportXML] Opening file:" << filePath;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "[ImportXML] ERROR: Cannot open file";
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
return {};
}
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
QXmlStreamReader xml(&file);
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
NodeTree tree;
tree.baseAddress = 0x00400000;
tree.pointerSize = pointerSize;
// Class name → struct node ID (for pointer resolution)
QHash<QString, uint64_t> classIds;
// Deferred pointer refs to resolve after all classes are parsed
QVector<PendingRef> pendingRefs;
// Detect version from first comment
bool versionDetected = false;
while (!xml.atEnd()) {
xml.readNext();
// Detect version from XML comments
if (!versionDetected && xml.isComment()) {
QString comment = xml.text().toString().trimmed();
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
version = XmlVersion::V2016;
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
version = XmlVersion::V2013;
}
// else keep default V2016
versionDetected = true;
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
}
if (!xml.isStartElement()) continue;
if (xml.name() == QStringLiteral("Class")) {
// Parse a class element into a root Struct node
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
// Create root struct node (collapsed by default for large files)
Node structNode;
structNode.kind = NodeKind::Struct;
structNode.name = className;
structNode.structTypeName = className;
structNode.parentId = 0; // root level
structNode.offset = 0;
structNode.collapsed = true;
int structIdx = tree.addNode(structNode);
uint64_t structId = tree.nodes[structIdx].id;
classIds[className] = structId;
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
// Parse child Node elements
int childOffset = 0;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
break;
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
continue;
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
// Handle Custom type: expand to appropriate hex nodes
if (isCustomType(xmlType, version) && nodeSize > 0) {
// Pick best-fit hex kind
NodeKind hexKind;
int hexSize;
if (nodeSize >= 8 && nodeSize % 8 == 0) {
hexKind = NodeKind::Hex64; hexSize = 8;
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
hexKind = NodeKind::Hex32; hexSize = 4;
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
hexKind = NodeKind::Hex16; hexSize = 2;
} else {
hexKind = NodeKind::Hex8; hexSize = 1;
}
int count = nodeSize / hexSize;
for (int i = 0; i < count; i++) {
Node n;
n.kind = hexKind;
n.name = (count == 1) ? nodeName : QString();
n.parentId = structId;
n.offset = childOffset;
tree.addNode(n);
childOffset += hexSize;
}
continue;
}
NodeKind kind = lookupKind(xmlType, version, pointerSize);
// Handle ClassInstanceArray: read child <Array> element
if (isClassInstanceArrayType(xmlType, version)) {
qDebug() << "[ImportXML] -> ClassInstanceArray";
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
if (total <= 0)
total = xml.attributes().value(QStringLiteral("Count")).toInt();
if (total <= 0) total = 1;
// Read child <Array> element for class name
QString arrayClassName;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
break;
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
if (arrayTotal <= 0)
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
if (arrayTotal > 0) total = arrayTotal;
}
}
// Create an Array node wrapping Struct elements
Node arrNode;
arrNode.kind = NodeKind::Array;
arrNode.name = nodeName;
arrNode.parentId = structId;
arrNode.offset = childOffset;
arrNode.arrayLen = total;
arrNode.elementKind = NodeKind::Struct;
if (!arrayClassName.isEmpty())
arrNode.structTypeName = arrayClassName;
int arrIdx = tree.addNode(arrNode);
uint64_t arrId = tree.nodes[arrIdx].id;
// Defer ref resolution if array references a class
if (!arrayClassName.isEmpty()) {
pendingRefs.append({arrId, arrayClassName});
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
Node n;
n.kind = kind;
n.name = nodeName;
n.parentId = structId;
n.offset = childOffset;
// Handle text nodes
if (isTextType(xmlType, version)) {
if (isUtf16TextType(xmlType, version))
n.strLen = qMax(1, nodeSize / 2);
else
n.strLen = qMax(1, nodeSize);
}
// Handle pointer types
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, ptrClass});
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
continue;
}
// Handle embedded class instance
if (isClassInstanceType(xmlType, version)) {
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
n.structTypeName = resolvedClass;
if (!n.structTypeName.isEmpty()) {
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, n.structTypeName});
} else {
tree.addNode(n);
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
tree.addNode(n);
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
}
}
}
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
if (errorMsg)
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
.arg(xml.lineNumber())
.arg(xml.errorString());
return {};
}
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
if (tree.nodes.isEmpty()) {
qDebug() << "[ImportXML] ERROR: No classes found";
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
return {};
}
// Resolve deferred pointer/struct references
int resolved = 0, unresolved = 0;
for (const auto& ref : pendingRefs) {
int nodeIdx = tree.indexOfId(ref.nodeId);
if (nodeIdx < 0) continue;
auto it = classIds.find(ref.className);
if (it != classIds.end()) {
tree.nodes[nodeIdx].refId = it.value();
resolved++;
} else {
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
unresolved++;
}
}
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
return tree;
}
} // namespace rcx

View File

@@ -0,0 +1,13 @@
#pragma once
#include "core.h"
namespace rcx {
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr,
int pointerSize = 8);
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
#pragma once
#include "core.h"
namespace rcx {
// Import C/C++ struct definitions from source code into a NodeTree.
// Supports two modes (auto-detected):
// 1. With comment offsets (// 0xNN) - trusts the offset values
// 2. Without comment offsets - computes offsets from type sizes
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr,
int pointerSize = 8);
} // namespace rcx

View File

@@ -66,7 +66,8 @@ struct PluginProcessInfo {
QString name;
QString path;
QIcon icon;
bool is32Bit = false;
PluginProcessInfo() : pid(0) {}
PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon())
: pid(p), name(n), path(pth), icon(i) {}

13
src/macos_titlebar.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <QWidget>
namespace rcx {
struct Theme;
// Apply macOS native title bar color to match the theme.
// No-op on non-macOS platforms (implementation is platform-specific).
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
} // namespace rcx

43
src/macos_titlebar.mm Normal file
View File

@@ -0,0 +1,43 @@
#include "macos_titlebar.h"
#include "themes/theme.h"
#import <Cocoa/Cocoa.h>
#include <QColor>
#include <QWidget>
namespace rcx {
static NSColor* toNSColor(const QColor& color) {
return [NSColor colorWithCalibratedRed:color.redF()
green:color.greenF()
blue:color.blueF()
alpha:color.alphaF()];
}
void applyMacTitleBarTheme(QWidget* window, const Theme& theme) {
if (!window) return;
// Ensure native window is created.
window->winId();
auto* nsView = reinterpret_cast<NSView*>(window->winId());
if (!nsView) return;
NSWindow* nsWindow = [nsView window];
if (!nsWindow) return;
// Keep native traffic lights while tinting the title bar to the theme.
// Match the title text contrast by selecting the appropriate system appearance.
const qreal luminance =
0.2126 * theme.background.redF() +
0.7152 * theme.background.greenF() +
0.0722 * theme.background.blueF();
const bool isLight = luminance >= 0.5;
[nsWindow setAppearance:[NSAppearance appearanceNamed:
(isLight ? NSAppearanceNameAqua : NSAppearanceNameDarkAqua)]];
[nsWindow setTitlebarAppearsTransparent:YES];
[nsWindow setTitleVisibility:NSWindowTitleVisible];
[nsWindow setBackgroundColor:toNSColor(theme.background)];
}
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
#include "controller.h"
#include "titlebar.h"
#include "pluginmanager.h"
#include "scannerpanel.h"
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
@@ -11,27 +12,35 @@
#include <QDockWidget>
#include <QTreeView>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QLineEdit>
#include <QMap>
#include <QButtonGroup>
#include <QPushButton>
#include <QTimer>
#include <Qsci/qsciscintilla.h>
namespace rcx {
class McpBridge;
class ShimmerLabel;
class MainWindow : public QMainWindow {
Q_OBJECT
friend class McpBridge;
public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow() override;
private slots:
void newFile();
void newDocument();
void newClass();
void newStruct();
void newEnum();
void selfTest();
void openFile();
void saveFile();
void saveFileAs();
void closeFile();
void addNode();
void removeNode();
@@ -47,12 +56,22 @@ private slots:
void toggleMcp();
void setEditorFont(const QString& fontName);
void exportCpp();
void exportReclassXmlAction();
void importFromSource();
void importReclassXml();
void importPdb();
void showTypeAliasesDialog();
void editTheme();
void showOptionsDialog();
public:
// Status bar helpers — separate app / MCP channels
void setAppStatus(const QString& text);
void setMcpStatus(const QString& text);
void clearMcpStatus();
// Project Lifecycle API
QMdiSubWindow* project_new();
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
@@ -61,17 +80,31 @@ private:
enum ViewMode { VM_Reclass, VM_Rendered };
QMdiArea* m_mdiArea;
QLabel* m_statusLabel;
ShimmerLabel* m_statusLabel;
QString m_appStatus;
bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr;
QMenuBar* m_menuBar = nullptr;
bool m_menuBarTitleCase = false;
QWidget* m_borderOverlay = nullptr;
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QAction* m_removeSplitAction = nullptr;
QMenu* m_sourceMenu = nullptr;
QMenu* m_recentFilesMenu = nullptr;
struct SplitPane {
QTabWidget* tabWidget = nullptr;
RcxEditor* editor = nullptr;
QsciScintilla* rendered = nullptr;
QLineEdit* findBar = nullptr;
QWidget* findContainer = nullptr;
QWidget* renderedContainer = nullptr;
ViewMode viewMode = VM_Reclass;
uint64_t lastRenderedRootId = 0;
};
@@ -84,11 +117,16 @@ private:
int activePaneIdx = 0;
};
QMap<QMdiSubWindow*, TabState> m_tabs;
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
void rebuildAllDocs();
void createMenus();
void applyMenuBarTitleCase(bool titleCase);
void createStatusBar();
void showPluginsDialog();
void populateSourceMenu();
void addRecentFile(const QString& path);
void updateRecentFilesMenu();
QIcon makeIcon(const QString& svgPath);
RcxController* activeController() const;
@@ -106,20 +144,31 @@ private:
SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme);
void applyTabWidgetStyle(QTabWidget* tw);
void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();
RcxEditor* activePaneEditor();
// Workspace dock
QDockWidget* m_workspaceDock = nullptr;
QTreeView* m_workspaceTree = nullptr;
QStandardItemModel* m_workspaceModel = nullptr;
QDockWidget* m_workspaceDock = nullptr;
QTreeView* m_workspaceTree = nullptr;
QStandardItemModel* m_workspaceModel = nullptr;
QSortFilterProxyModel* m_workspaceProxy = nullptr;
QLineEdit* m_workspaceSearch = nullptr;
QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color);
// Scanner dock
QDockWidget* m_scannerDock = nullptr;
ScannerPanel* m_scannerPanel = nullptr;
QLabel* m_scanDockTitle = nullptr;
QToolButton* m_scanDockCloseBtn = nullptr;
void createScannerDock();
protected:
void changeEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;

View File

@@ -4,6 +4,7 @@
#include "generator.h"
#include "mainwindow.h"
#include <QCoreApplication>
#include <QSettings>
#include <QDebug>
#include <cstring>
@@ -170,9 +171,15 @@ void McpBridge::processLine(const QByteArray& line) {
}
if (method == "initialize") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
QCoreApplication::processEvents();
sendJson(handleInitialize(id, req.value("params").toObject()));
m_mainWindow->clearMcpStatus();
} else if (method == "tools/list") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
QCoreApplication::processEvents();
sendJson(handleToolsList(id));
m_mainWindow->clearMcpStatus();
} else if (method == "tools/call") {
sendJson(handleToolsCall(id, req.value("params").toObject()));
} else {
@@ -211,20 +218,29 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
// 1. project.state
tools.append(QJsonObject{
{"name", "project.state"},
{"description", "Returns project state: node tree, base address, sources, provider info. "
"Use depth/parentId to avoid dumping the whole tree. "
"Call with depth:1 first to see top-level structs, then drill in with parentId."},
{"description", "Returns project state with paginated node tree. "
"Responses return max 'limit' nodes (default 50). "
"Use depth:1 first, then parentId to drill into a struct. "
"Enum/bitfield member arrays are omitted by default (counts shown instead); "
"pass includeMembers:true to get full arrays. "
"Response includes returned/total/nextOffset for paging."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}},
{"depth", QJsonObject{{"type", "integer"},
{"description", "Max tree depth to return (default 1 = top-level structs only)."}}},
{"description", "Max tree depth to return (default 1)."}}},
{"parentId", QJsonObject{{"type", "string"},
{"description", "Only return children of this node."}}},
{"includeTree", QJsonObject{{"type", "boolean"},
{"description", "If false, return only provider/source info, no tree. Default true."}}}
{"description", "If false, return only provider/source info, no tree. Default true."}}},
{"includeMembers", QJsonObject{{"type", "boolean"},
{"description", "If true, include full enumMembers/bitfieldMembers arrays. Default false (shows counts only)."}}},
{"limit", QJsonObject{{"type", "integer"},
{"description", "Max nodes to return (default 50, max 500)."}}},
{"offset", QJsonObject{{"type", "integer"},
{"description", "Skip this many nodes (for pagination). Use nextOffset from previous response."}}}
}}
}}
});
@@ -240,7 +256,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
"insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. "
"change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
"change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
"change_base: {op:'change_base', baseAddress:'0x400000'}. "
"change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. "
"change_struct_type: {op:'change_struct_type', nodeId:'ID', structTypeName:'Name'}. "
"change_class_keyword: {op:'change_class_keyword', nodeId:'ID', classKeyword:'class'}. "
"change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
@@ -248,7 +264,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
"Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Padding Struct Array"},
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
@@ -287,7 +303,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"name", "hex.read"},
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
"Offset is provider-relative (0-based) unless baseRelative=true."},
"Offset is tree-relative (0-based, baseAddress added automatically) "
"unless baseRelative=true (offset is absolute)."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
@@ -342,7 +359,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
"select_node, refresh"},
"select_node, refresh. "
"export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
@@ -356,6 +374,46 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
}}
});
// 8. tree.search
tools.append(QJsonObject{
{"name", "tree.search"},
{"description", "Search for nodes by name (substring, case-insensitive). "
"Returns compact results: id, name, kind, parentId, offset, childCount. "
"Use kindFilter to narrow (e.g. 'Struct'). Max 100 results. "
"Much faster than paging through project.state to find a specific type."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}},
{"query", QJsonObject{{"type", "string"},
{"description", "Name substring to search for (case-insensitive)."}}},
{"kindFilter", QJsonObject{{"type", "string"},
{"description", "Filter by node kind (e.g. 'Struct', 'Hex64', 'Array')."}}},
{"limit", QJsonObject{{"type", "integer"},
{"description", "Max results to return (default 20, max 100)."}}}
}}
}}
});
// 9. node.history
tools.append(QJsonObject{
{"name", "node.history"},
{"description", "Returns timestamped value change history (up to 10 entries) "
"for specified nodes. Requires live provider with value tracking enabled."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"nodeIds", QJsonObject{{"type", "array"},
{"items", QJsonObject{{"type", "string"}}},
{"description", "Array of node IDs to get history for."}}},
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index. Omit for active tab."}}}
}},
{"required", QJsonArray{"nodeIds"}}
}}
});
return okReply(id, QJsonObject{{"tools", tools}});
}
@@ -367,6 +425,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
QString toolName = params.value("name").toString();
QJsonObject args = params.value("arguments").toObject();
// Show tool activity in status bar (with shimmer)
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
QCoreApplication::processEvents(); // paint immediately
QJsonObject result;
if (toolName == "project.state") result = toolProjectState(args);
else if (toolName == "tree.apply") result = toolTreeApply(args);
@@ -375,8 +437,12 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
else if (toolName == "hex.write") result = toolHexWrite(args);
else if (toolName == "status.set") result = toolStatusSet(args);
else if (toolName == "ui.action") result = toolUiAction(args);
else if (toolName == "tree.search") result = toolTreeSearch(args);
else if (toolName == "node.history") result = toolNodeHistory(args);
else return errReply(id, -32601, "Unknown tool: " + toolName);
m_mainWindow->clearMcpStatus();
return okReply(id, result);
}
@@ -435,6 +501,9 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
int maxDepth = args.value("depth").toInt(1);
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
bool includeMembers = args.value("includeMembers").toBool(false);
int limit = qBound(1, args.value("limit").toInt(50), 500);
int offset = qMax(0, args.value("offset").toInt(0));
QString parentIdStr = args.value("parentId").toString();
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
@@ -480,6 +549,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
state["modified"] = doc->modified;
state["undoAvailable"] = doc->undoStack.canUndo();
state["redoAvailable"] = doc->undoStack.canRedo();
state["statusText"] = m_mainWindow->m_appStatus;
// Filtered tree: only emit nodes up to maxDepth from the filter root
if (includeTree) {
@@ -488,12 +558,15 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
for (int i = 0; i < tree.nodes.size(); i++)
childMap[tree.nodes[i].parentId].append(i);
// BFS from filterParentId, respecting maxDepth
// BFS from filterParentId, respecting maxDepth + pagination
QJsonArray nodeArr;
struct QueueEntry { uint64_t parentId; int depth; };
QVector<QueueEntry> queue;
queue.append({filterParentId, 0});
int totalCount = 0; // total nodes that match depth filter
int emitted = 0;
while (!queue.isEmpty()) {
auto entry = queue.takeFirst();
if (entry.depth > maxDepth) continue;
@@ -501,13 +574,47 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
const auto& kids = childMap.value(entry.parentId);
for (int ci : kids) {
const Node& n = tree.nodes[ci];
// Count all matching nodes for pagination metadata
totalCount++;
// Apply offset/limit pagination
if (totalCount <= offset) {
// Still skipping — but enqueue children for counting
if (entry.depth + 1 <= maxDepth)
queue.append({n.id, entry.depth + 1});
continue;
}
if (emitted >= limit) {
// Past limit — just keep counting total
if (entry.depth + 1 <= maxDepth)
queue.append({n.id, entry.depth + 1});
continue;
}
QJsonObject nj = n.toJson();
// Strip inline member arrays unless requested
if (!includeMembers) {
if (nj.contains("enumMembers")) {
int count = nj.value("enumMembers").toArray().size();
nj.remove("enumMembers");
nj["enumMemberCount"] = count;
}
if (nj.contains("bitfieldMembers")) {
int count = nj.value("bitfieldMembers").toArray().size();
nj.remove("bitfieldMembers");
nj["bitfieldMemberCount"] = count;
}
}
// Add computed size for containers
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
nj["computedSize"] = tree.structSpan(n.id, &childMap);
nj["childCount"] = childMap.value(n.id).size();
}
nodeArr.append(nj);
emitted++;
// Enqueue children if we haven't hit depth limit
if (entry.depth + 1 <= maxDepth)
@@ -519,6 +626,10 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
treeObj["nextId"] = QString::number(tree.m_nextId);
treeObj["nodes"] = nodeArr;
treeObj["returned"] = emitted;
treeObj["total"] = totalCount;
if (emitted < totalCount)
treeObj["nextOffset"] = offset + emitted;
state["tree"] = treeObj;
}
@@ -659,8 +770,10 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
}
else if (opType == "change_base") {
uint64_t newBase = op.value("baseAddress").toString().toULongLong(nullptr, 16);
QString oldFormula = tree.baseAddressFormula;
QString newFormula = op.value("formula").toString();
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeBase{tree.baseAddress, newBase}));
cmd::ChangeBase{tree.baseAddress, newBase, oldFormula, newFormula}));
applied++;
}
else if (opType == "change_struct_type") {
@@ -793,7 +906,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
}
if (args.contains("pid")) {
uint32_t pid = (uint32_t)args.value("pid").toInteger();
uint32_t pid = (uint32_t)args.value("pid").toInt();
QString name = args.value("processName").toString();
if (name.isEmpty()) name = QString("PID %1").arg(pid);
QString target = QString("%1:%2").arg(pid).arg(name);
@@ -825,8 +938,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int length = qMin(args.value("length").toInt(64), 4096);
if (args.value("baseRelative").toBool())
offset -= (int64_t)tab->doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)tab->doc->tree.baseAddress;
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
@@ -907,8 +1020,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
QString hexStr = args.value("hexBytes").toString().remove(' ');
if (args.value("baseRelative").toBool())
offset -= (int64_t)doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)doc->tree.baseAddress;
if (hexStr.size() % 2 != 0)
return makeTextResult("Hex string must have even length", true);
@@ -955,7 +1068,7 @@ QJsonObject McpBridge::toolStatusSet(const QJsonObject& args) {
}
}
if (target == "statusBar" || target == "both") {
m_mainWindow->m_statusLabel->setText(text);
m_mainWindow->setAppStatus(text);
}
return makeTextResult("Status set: " + text);
@@ -1003,7 +1116,25 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
if (action == "export_cpp") {
if (!doc) return makeTextResult("No active tab", true);
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
QString code = renderCppAll(doc->tree, aliases);
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
QString code;
if (!nodeIdStr.isEmpty()) {
// Per-struct export
uint64_t nid = nodeIdStr.toULongLong();
code = renderCpp(doc->tree, nid, aliases, asserts);
if (code.isEmpty())
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
} else {
code = renderCppAll(doc->tree, aliases, asserts);
}
// Truncate if too large (64 KB limit)
if (code.size() > 65536) {
int totalSize = code.size();
code.truncate(65536);
code += QStringLiteral("\n\n... truncated (%1 bytes total, showing first 64KB)"
"\nUse nodeId param to export a single struct.")
.arg(totalSize);
}
return makeTextResult(code);
}
if (action == "save_file") {
@@ -1052,6 +1183,107 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
return makeTextResult("Unknown action: " + action, true);
}
// ════════════════════════════════════════════════════════════════════
// TOOL: tree.search
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab", true);
const auto& tree = tab->doc->tree;
QString query = args.value("query").toString();
QString kindFilter = args.value("kindFilter").toString();
int limit = qBound(1, args.value("limit").toInt(20), 100);
if (query.isEmpty() && kindFilter.isEmpty())
return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true);
// Build parent→children map for childCount
QHash<uint64_t, int> childCounts;
for (const auto& n : tree.nodes)
childCounts[n.parentId]++;
QJsonArray results;
for (const auto& n : tree.nodes) {
// Kind filter
if (!kindFilter.isEmpty()) {
if (kindToString(n.kind) != kindFilter) continue;
}
// Name substring match (case-insensitive)
if (!query.isEmpty()) {
bool nameMatch = n.name.contains(query, Qt::CaseInsensitive);
bool typeMatch = n.structTypeName.contains(query, Qt::CaseInsensitive);
if (!nameMatch && !typeMatch) continue;
}
QJsonObject nj;
nj["id"] = QString::number(n.id);
nj["name"] = n.name;
nj["kind"] = kindToString(n.kind);
nj["parentId"] = QString::number(n.parentId);
nj["offset"] = n.offset;
if (!n.structTypeName.isEmpty())
nj["structTypeName"] = n.structTypeName;
if (!n.classKeyword.isEmpty())
nj["classKeyword"] = n.classKeyword;
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array)
nj["childCount"] = childCounts.value(n.id, 0);
if (!n.enumMembers.isEmpty())
nj["enumMemberCount"] = n.enumMembers.size();
if (!n.bitfieldMembers.isEmpty())
nj["bitfieldMemberCount"] = n.bitfieldMembers.size();
results.append(nj);
if (results.size() >= limit) break;
}
QJsonObject out;
out["results"] = results;
out["count"] = results.size();
out["query"] = query;
if (!kindFilter.isEmpty()) out["kindFilter"] = kindFilter;
return makeTextResult(QString::fromUtf8(
QJsonDocument(out).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════
// Tool: node.history — return timestamped value history for nodes
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab.", true);
const auto& histMap = tab->ctrl->valueHistory();
QJsonArray requestedIds = args.value("nodeIds").toArray();
if (requestedIds.isEmpty())
return makeTextResult("nodeIds array is required.", true);
QJsonObject result;
for (const auto& idVal : requestedIds) {
QString idStr = idVal.toString();
uint64_t nodeId = idStr.toULongLong();
auto it = histMap.find(nodeId);
QJsonArray entries;
if (it != histMap.end()) {
it->forEachWithTime([&](const QString& val, qint64 msec) {
QJsonObject entry;
entry.insert(QStringLiteral("value"), val);
entry.insert(QStringLiteral("timestamp"), msec);
entries.append(entry);
});
}
QJsonObject nodeResult;
nodeResult.insert(QStringLiteral("entries"), entries);
nodeResult.insert(QStringLiteral("heatLevel"), it != histMap.end() ? it->heatLevel() : 0);
nodeResult.insert(QStringLiteral("uniqueCount"), it != histMap.end() ? it->uniqueCount() : 0);
result.insert(idStr, nodeResult);
}
return makeTextResult(QString::fromUtf8(
QJsonDocument(result).toJson(QJsonDocument::Compact)));
}
// ════════════════════════════════════════════════════════════════════
// Notifications (call from MainWindow/Controller hooks)
// ════════════════════════════════════════════════════════════════════

View File

@@ -58,6 +58,8 @@ private:
QJsonObject toolHexWrite(const QJsonObject& args);
QJsonObject toolStatusSet(const QJsonObject& args);
QJsonObject toolUiAction(const QJsonObject& args);
QJsonObject toolTreeSearch(const QJsonObject& args);
QJsonObject toolNodeHistory(const QJsonObject& args);
// Helpers
QJsonObject makeTextResult(const QString& text, bool isError = false);

270
src/optionsdialog.cpp Normal file
View File

@@ -0,0 +1,270 @@
#include "optionsdialog.h"
#include "themes/thememanager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QGroupBox>
#include <QLabel>
#include <QTreeWidgetItem>
#include <functional>
namespace rcx {
OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
: QDialog(parent)
{
setWindowTitle("Options");
setFixedSize(700, 450);
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(8);
mainLayout->setContentsMargins(10, 10, 10, 10);
// -- Middle: left column (search + tree) | right column (pages) --
auto* middleLayout = new QHBoxLayout;
middleLayout->setSpacing(8);
// Left column: search bar + tree
auto* leftColumn = new QVBoxLayout;
leftColumn->setSpacing(4);
m_search = new QLineEdit;
m_search->setPlaceholderText("Search Options (Ctrl+E)");
m_search->setClearButtonEnabled(true);
connect(m_search, &QLineEdit::textChanged, this, &OptionsDialog::filterTree);
leftColumn->addWidget(m_search);
m_tree = new QTreeWidget;
m_tree->setHeaderHidden(true);
m_tree->setRootIsDecorated(true);
m_tree->setFixedWidth(200);
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
m_tree->expandAll();
m_tree->setCurrentItem(generalItem);
leftColumn->addWidget(m_tree, 1);
middleLayout->addLayout(leftColumn);
// Right column: stacked pages with group boxes
m_pages = new QStackedWidget;
// -- General page --
auto* generalPage = new QWidget;
auto* generalLayout = new QVBoxLayout(generalPage);
generalLayout->setContentsMargins(0, 0, 0, 0);
generalLayout->setSpacing(8);
// Refresh Rate group box
auto* refreshGroup = new QGroupBox("Refresh Rate");
auto* refreshLayout = new QFormLayout(refreshGroup);
refreshLayout->setSpacing(8);
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_refreshSpin = new QSpinBox;
m_refreshSpin->setRange(1, 60000);
m_refreshSpin->setSingleStep(50);
m_refreshSpin->setValue(current.refreshMs);
m_refreshSpin->setSuffix(" ms");
m_refreshSpin->setObjectName("refreshSpin");
refreshLayout->addRow("Interval:", m_refreshSpin);
auto* refreshDesc = new QLabel(
"How often live memory is re-read and the view is updated, in milliseconds. "
"Lower values give faster updates but use more CPU. Default: 660 ms.");
refreshDesc->setWordWrap(true);
refreshDesc->setContentsMargins(0, 0, 0, 0);
refreshLayout->addRow(refreshDesc);
generalLayout->addWidget(refreshGroup);
// Visual Experience group box
auto* visualGroup = new QGroupBox("Visual Experience");
auto* visualLayout = new QFormLayout(visualGroup);
visualLayout->setSpacing(8);
visualLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_themeCombo = new QComboBox;
auto& tm = ThemeManager::instance();
for (const auto& theme : tm.themes())
m_themeCombo->addItem(theme.name);
m_themeCombo->setCurrentIndex(current.themeIndex);
m_themeCombo->setObjectName("themeCombo");
visualLayout->addRow("Color theme:", m_themeCombo);
m_fontCombo = new QComboBox;
m_fontCombo->addItem("JetBrains Mono");
m_fontCombo->addItem("Consolas");
m_fontCombo->setCurrentText(current.fontName);
m_fontCombo->setObjectName("fontCombo");
visualLayout->addRow("Editor Font:", m_fontCombo);
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
visualLayout->addRow(m_titleCaseCheck);
m_showIconCheck = new QCheckBox("Show icon in title bar");
m_showIconCheck->setChecked(current.showIcon);
visualLayout->addRow(m_showIconCheck);
generalLayout->addWidget(visualGroup);
// Safe Mode group box
auto* safeModeGroup = new QGroupBox("Preview Features");
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
safeModeLayout->setSpacing(4);
m_safeModeCheck = new QCheckBox("Safe Mode");
m_safeModeCheck->setChecked(current.safeMode);
safeModeLayout->addWidget(m_safeModeCheck);
auto* safeModeDesc = new QLabel(
"Enable to use the default OS icon for this application and "
"create the window with the name of the executable file.");
safeModeDesc->setWordWrap(true);
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
safeModeLayout->addWidget(safeModeDesc);
generalLayout->addWidget(safeModeGroup);
generalLayout->addStretch();
m_pages->addWidget(generalPage); // index 0
m_pageKeywords[generalItem] = collectPageKeywords(generalPage);
// -- AI Features page --
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
auto* aiPage = new QWidget;
auto* aiLayout = new QVBoxLayout(aiPage);
aiLayout->setContentsMargins(0, 0, 0, 0);
aiLayout->setSpacing(8);
auto* mcpGroup = new QGroupBox("MCP Server");
auto* mcpLayout = new QVBoxLayout(mcpGroup);
mcpLayout->setSpacing(4);
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
m_autoMcpCheck->setChecked(current.autoStartMcp);
mcpLayout->addWidget(m_autoMcpCheck);
auto* mcpDesc = new QLabel(
"Automatically start the MCP bridge server when the application launches, "
"allowing external AI tools to connect and interact with the editor.");
mcpDesc->setWordWrap(true);
mcpDesc->setContentsMargins(20, 0, 0, 0);
mcpLayout->addWidget(mcpDesc);
aiLayout->addWidget(mcpGroup);
aiLayout->addStretch();
m_pages->addWidget(aiPage); // index 1
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
// -- Generator page --
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
auto* generatorPage = new QWidget;
auto* generatorLayout = new QVBoxLayout(generatorPage);
generatorLayout->setContentsMargins(0, 0, 0, 0);
generatorLayout->setSpacing(8);
auto* cppGroup = new QGroupBox("C++ Header");
auto* cppLayout = new QVBoxLayout(cppGroup);
m_assertCheck = new QCheckBox("Emit static_assert size checks");
m_assertCheck->setChecked(current.generatorAsserts);
cppLayout->addWidget(m_assertCheck);
generatorLayout->addWidget(cppGroup);
generatorLayout->addStretch();
m_pages->addWidget(generatorPage); // index 2
m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage);
middleLayout->addWidget(m_pages, 1);
mainLayout->addLayout(middleLayout, 1);
// Tree <-> page connection
m_itemPageIndex[generalItem] = 0;
m_itemPageIndex[aiItem] = 1;
m_itemPageIndex[generatorItem] = 2;
connect(m_tree, &QTreeWidget::currentItemChanged, this,
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
auto it = m_itemPageIndex.find(item);
if (it != m_itemPageIndex.end())
m_pages->setCurrentIndex(it.value());
});
// -- Button box --
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttons);
}
OptionsResult OptionsDialog::result() const {
OptionsResult r;
r.themeIndex = m_themeCombo->currentIndex();
r.fontName = m_fontCombo->currentText();
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
r.showIcon = m_showIconCheck->isChecked();
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
r.generatorAsserts = m_assertCheck->isChecked();
return r;
}
QStringList OptionsDialog::collectPageKeywords(QWidget* page) {
QStringList keywords;
for (auto* child : page->findChildren<QWidget*>()) {
if (auto* label = qobject_cast<QLabel*>(child))
keywords << label->text();
else if (auto* cb = qobject_cast<QCheckBox*>(child))
keywords << cb->text();
else if (auto* gb = qobject_cast<QGroupBox*>(child))
keywords << gb->title();
else if (auto* combo = qobject_cast<QComboBox*>(child)) {
for (int i = 0; i < combo->count(); ++i)
keywords << combo->itemText(i);
}
}
return keywords;
}
void OptionsDialog::filterTree(const QString& text) {
std::function<bool(QTreeWidgetItem*)> filter = [&](QTreeWidgetItem* item) -> bool {
bool anyChildVisible = false;
for (int i = 0; i < item->childCount(); ++i) {
if (filter(item->child(i)))
anyChildVisible = true;
}
bool selfMatch = item->text(0).contains(text, Qt::CaseInsensitive);
if (!selfMatch) {
for (const auto& kw : m_pageKeywords.value(item)) {
if (kw.contains(text, Qt::CaseInsensitive)) {
selfMatch = true;
break;
}
}
}
bool visible = selfMatch || anyChildVisible;
item->setHidden(!visible);
if (visible && item->childCount() > 0)
item->setExpanded(true);
return visible;
};
for (int i = 0; i < m_tree->topLevelItemCount(); ++i)
filter(m_tree->topLevelItem(i));
}
} // namespace rcx

53
src/optionsdialog.h Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include <QDialog>
#include <QLineEdit>
#include <QTreeWidget>
#include <QStackedWidget>
#include <QComboBox>
#include <QCheckBox>
#include <QHash>
#include <QSpinBox>
namespace rcx {
struct OptionsResult {
int themeIndex = 0;
QString fontName;
bool menuBarTitleCase = true;
bool showIcon = false;
bool safeMode = false;
bool autoStartMcp = true;
int refreshMs = 660;
bool generatorAsserts = false;
};
class OptionsDialog : public QDialog {
Q_OBJECT
public:
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
OptionsResult result() const;
private:
void filterTree(const QString& text);
static QStringList collectPageKeywords(QWidget* page);
QLineEdit* m_search = nullptr;
QTreeWidget* m_tree = nullptr;
QStackedWidget* m_pages = nullptr;
QComboBox* m_themeCombo = nullptr;
QComboBox* m_fontCombo = nullptr;
QCheckBox* m_titleCaseCheck = nullptr;
QCheckBox* m_showIconCheck = nullptr;
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QSpinBox* m_refreshSpin = nullptr;
QCheckBox* m_assertCheck = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
// tree item → stacked widget page index
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
};
} // namespace rcx

View File

@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
QString name = QString::fromStdString(plugin->Name());
QString identifier = name.toLower().replace(" ", "");
ProviderRegistry::instance().registerProvider(name, identifier, provider);
QString dllFileName = QFileInfo(path).fileName();
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
}
return true;

View File

@@ -5,6 +5,10 @@
#include <QMessageBox>
#include <QFileInfo>
#include <QPixmap>
#include <QSettings>
#include <QApplication>
#include <QClipboard>
#include <QMenu>
#ifdef _WIN32
#include <windows.h>
@@ -27,22 +31,9 @@ ProcessPicker::ProcessPicker(QWidget *parent)
, m_useCustomList(false)
{
ui->setupUi(this);
// Configure table
ui->processTable->setColumnWidth(0, 80); // PID column - fixed width
ui->processTable->setColumnWidth(1, 200); // Name column - fixed width
ui->processTable->horizontalHeader()->setStretchLastSection(true); // Path column - fills remaining space
ui->processTable->setWordWrap(false); // Disable word wrap for single-line display
ui->processTable->setTextElideMode(Qt::ElideLeft); // Elide from left (show end of path)
// Connect signals
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
// Initial process enumeration
initUi();
refreshProcessList();
selectPreferredProcess();
}
ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget *parent)
@@ -51,23 +42,102 @@ ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget
, m_useCustomList(true)
{
ui->setupUi(this);
// Configure table
ui->processTable->setColumnWidth(0, 80);
ui->processTable->setColumnWidth(1, 200);
initUi();
ui->refreshButton->setVisible(false);
m_allProcesses = customProcesses;
applyFilter();
selectPreferredProcess();
}
void ProcessPicker::initUi()
{
// Table configuration
ui->processTable->setColumnWidth(0, 80); // PID column
ui->processTable->setColumnWidth(1, 200); // Name column
ui->processTable->horizontalHeader()->setStretchLastSection(true);
ui->processTable->setWordWrap(false);
ui->processTable->setTextElideMode(Qt::ElideLeft);
// Connect signals (no refresh button for custom lists)
ui->refreshButton->setVisible(false);
ui->processTable->setShowGrid(false);
ui->processTable->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 6);
// Signal connections
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
// Use custom process list
m_allProcesses = customProcesses;
applyFilter();
// Derive theme colors from the global palette (set by applyGlobalTheme)
QPalette pal = qApp->palette();
QString bg = pal.color(QPalette::Base).name();
QString text = pal.color(QPalette::Text).name();
QString hover = pal.color(QPalette::Mid).name();
QString surface = pal.color(QPalette::AlternateBase).name();
QString button = pal.color(QPalette::Button).name();
QString highlight= pal.color(QPalette::Highlight).name();
QString border = pal.color(QPalette::Mid).darker(120).name();
QString mutedText= pal.color(QPalette::Disabled, QPalette::WindowText).name();
QString hoverDk = pal.color(QPalette::Mid).darker(130).name();
ui->processTable->setStyleSheet(QStringLiteral(
"QTableWidget { background: %1; color: %2; border: none; }"
"QTableWidget::item { padding: 2px 6px; border: none; }"
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }")
.arg(bg, text, hover));
ui->processTable->horizontalHeader()->setStyleSheet(QStringLiteral(
"QHeaderView::section { background: %1; color: %2; border: none;"
" padding: 4px 6px; text-align: left; }")
.arg(surface, text));
ui->processTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->filterEdit->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
"QLineEdit:focus { border-color: %4; }")
.arg(bg, text, border, highlight));
QString btnStyle = QStringLiteral(
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
"QPushButton:hover { background: %4; }"
"QPushButton:pressed { background: %5; }"
"QPushButton:disabled { color: %6; }")
.arg(button, text, border, hover, hoverDk, mutedText);
ui->refreshButton->setStyleSheet(btnStyle);
ui->attachButton->setStyleSheet(btnStyle);
ui->cancelButton->setStyleSheet(btnStyle);
// Right-click context menu
ui->processTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->processTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
int row = ui->processTable->rowAt(pos.y());
if (row < 0) return;
auto* pidItem = ui->processTable->item(row, 0);
auto* nameItem = ui->processTable->item(row, 1);
auto* pathItem = ui->processTable->item(row, 2);
if (!pidItem || !nameItem) return;
QString pid = QString::number(pidItem->data(Qt::EditRole).toUInt());
QString name = nameItem->data(Qt::UserRole).toString();
QString path = pathItem ? pathItem->text() : QString();
QMenu menu;
auto* copyPid = menu.addAction(QStringLiteral("Copy PID"));
auto* copyName = menu.addAction(QStringLiteral("Copy Name"));
QAction* copyPath = nullptr;
if (!path.isEmpty())
copyPath = menu.addAction(QStringLiteral("Copy Path"));
auto* chosen = menu.exec(ui->processTable->viewport()->mapToGlobal(pos));
if (chosen == copyPid)
QApplication::clipboard()->setText(pid);
else if (chosen == copyName)
QApplication::clipboard()->setText(name);
else if (copyPath && chosen == copyPath)
QApplication::clipboard()->setText(path);
});
// Auto-focus filter for immediate typing
ui->filterEdit->setFocus();
}
ProcessPicker::~ProcessPicker()
@@ -97,28 +167,31 @@ void ProcessPicker::onProcessSelected()
{
auto* item = ui->processTable->currentItem();
if (!item) return;
int row = item->row();
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
m_selectedName = ui->processTable->item(row, 1)->text();
// Use original name stored in UserRole (without architecture suffix)
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
m_selectedName = origName.isValid() ? origName.toString()
: ui->processTable->item(row, 1)->text();
accept();
}
void ProcessPicker::enumerateProcesses()
{
QList<ProcessInfo> processes;
#ifdef _WIN32
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
QMessageBox::warning(this, "Error", "Failed to enumerate processes.");
return;
}
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(snapshot, &pe32))
{
do
@@ -126,10 +199,7 @@ void ProcessPicker::enumerateProcesses()
ProcessInfo info;
info.pid = pe32.th32ProcessID;
info.name = QString::fromWCharArray(pe32.szExeFile);
// Try to get full path and extract icon
// If we can't open a process with PROCESS_QUERY_LIMITED_INFORMATION then
// we for sure can't access their memory. - Skip in this case
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
if (hProcess)
{
@@ -140,7 +210,7 @@ void ProcessPicker::enumerateProcesses()
GetModuleFileNameExW(hProcess, nullptr, path, pathLen))
{
info.path = QString::fromWCharArray(path);
// Extract icon from executable
SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
@@ -158,11 +228,16 @@ void ProcessPicker::enumerateProcesses()
{
info.path = "";
}
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
processes.append(info);
}
} while (Process32NextW(snapshot, &pe32));
}
@@ -204,6 +279,16 @@ void ProcessPicker::enumerateProcesses()
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
// Detect 32-bit ELF process
QFile exeFile(exePath);
if (exeFile.open(QIODevice::ReadOnly)) {
QByteArray header = exeFile.read(5);
if (header.size() >= 5 && header[4] == 1) // ELFCLASS32
info.is32Bit = true;
exeFile.close();
}
processes.append(info);
}
#else
@@ -227,11 +312,16 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
pidItem->setData(Qt::EditRole, (int)proc.pid);
ui->processTable->setItem(i, 0, pidItem);
// Name column with icon
auto* nameItem = new QTableWidgetItem(proc.name);
// Name column with icon and architecture indicator
QString displayName = proc.is32Bit
? proc.name + QStringLiteral(" (32-bit)")
: proc.name;
auto* nameItem = new QTableWidgetItem(displayName);
if (!proc.icon.isNull()) {
nameItem->setIcon(proc.icon);
}
// Store original name for selectedProcessName()
nameItem->setData(Qt::UserRole, proc.name);
ui->processTable->setItem(i, 1, nameItem);
// Path column with tooltip for full path
@@ -269,3 +359,22 @@ void ProcessPicker::applyFilter()
populateTable(filtered);
}
void ProcessPicker::selectPreferredProcess()
{
// Try to select the last-attached process if it's in the list
QSettings s("Reclass", "Reclass");
QString lastProc = s.value("lastAttachedProcess").toString();
if (lastProc.isEmpty()) return;
for (int row = 0; row < ui->processTable->rowCount(); ++row) {
auto* nameItem = ui->processTable->item(row, 1);
if (!nameItem) continue;
QString name = nameItem->data(Qt::UserRole).toString();
if (name.compare(lastProc, Qt::CaseInsensitive) == 0) {
ui->processTable->selectRow(row);
ui->processTable->scrollToItem(nameItem);
break;
}
}
}

View File

@@ -14,6 +14,7 @@ struct ProcessInfo {
QString name;
QString path;
QIcon icon;
bool is32Bit = false;
};
class ProcessPicker : public QDialog
@@ -34,9 +35,11 @@ private slots:
void filterProcesses(const QString& text);
private:
void initUi();
void enumerateProcesses();
void populateTable(const QList<ProcessInfo>& processes);
void applyFilter();
void selectPreferredProcess();
Ui::ProcessPicker *ui;
uint32_t m_selectedPid = 0;

View File

@@ -127,22 +127,6 @@
</widget>
<resources/>
<connections>
<connection>
<sender>attachButton</sender>
<signal>clicked()</signal>
<receiver>ProcessPicker</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>600</x>
<y>470</y>
</hint>
<hint type="destinationlabel">
<x>350</x>
<y>250</y>
</hint>
</hints>
</connection>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>

View File

@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
return s_instance;
}
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
IProviderPlugin* plugin, const QString& dllFileName) {
// Check if already registered
for (const auto& info : m_providers) {
if (info.identifier == identifier) {
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
return;
}
}
m_providers.append(ProviderInfo(name, identifier, plugin));
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
}

View File

@@ -25,10 +25,13 @@ public:
IProviderPlugin* plugin; // Plugin (if plugin-based)
BuiltinFactory factory; // Factory (if built-in)
bool isBuiltin;
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
QString dllFileName; // Original DLL/SO filename (plugin-based only)
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
const QString& dll = {})
: name(n), identifier(id), plugin(p), factory(nullptr),
isBuiltin(false), dllFileName(dll) {}
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
};
@@ -36,7 +39,8 @@ public:
static ProviderRegistry& instance();
// Register a plugin-based provider
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
const QString& dllFileName = {});
// Register a built-in provider with a factory function
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);

View File

@@ -1,11 +1,21 @@
#pragma once
#include <QByteArray>
#include <QString>
#include <QVector>
#include <cstdint>
#include <cstring>
namespace rcx {
struct MemoryRegion {
uint64_t base = 0;
uint64_t size = 0;
bool readable = true;
bool writable = false;
bool executable = false;
QString moduleName;
};
class Provider {
public:
virtual ~Provider() = default;
@@ -33,10 +43,14 @@ public:
// Examples: "File", "Process", "Socket"
virtual QString kind() const { return QStringLiteral("File"); }
// Base address for providers that offset reads (e.g. process memory).
// Native pointer size of the target (4 for 32-bit, 8 for 64-bit).
// Providers should override this to report the target's architecture.
virtual int pointerSize() const { return 8; }
// Initial base address discovered by the provider (e.g. main module base).
// Used by the controller to set tree.baseAddress on first attach.
// For file/buffer providers this is always 0.
virtual uint64_t base() const { return 0; }
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
// Resolve an absolute address to a symbol name.
// Returns empty string if no symbol is known.
@@ -47,6 +61,18 @@ public:
return {};
}
// Resolve a module/symbol name to its address (reverse of getSymbol).
// Returns 0 if the name is not found.
virtual uint64_t symbolToAddress(const QString& name) const {
Q_UNUSED(name);
return 0;
}
// Enumerate committed/readable memory regions.
// Used by the scan engine to know what address ranges to scan.
// Default: returns empty (scan engine falls back to [0, size())).
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }

View File

@@ -67,6 +67,9 @@ public:
QString getSymbol(uint64_t addr) const override {
return m_real ? m_real->getSymbol(addr) : QString();
}
uint64_t symbolToAddress(const QString& n) const override {
return m_real ? m_real->symbolToAddress(n) : 0;
}
bool write(uint64_t addr, const void* buf, int len) override {
if (!m_real) return false;

View File

@@ -47,5 +47,18 @@
<file alias="selection.svg">vsicons/list-selection.svg</file>
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file>
<file alias="symbol-variable.svg">vsicons/symbol-variable.svg</file>
<file alias="server-process.svg">vsicons/server-process.svg</file>
<file alias="remote.svg">vsicons/remote.svg</file>
<file alias="plug.svg">vsicons/plug.svg</file>
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
<file alias="search.svg">vsicons/search.svg</file>
<file alias="regex.svg">vsicons/regex.svg</file>
<file alias="refresh.svg">vsicons/refresh.svg</file>
</qresource>
</RCC>

659
src/scanner.cpp Normal file
View File

@@ -0,0 +1,659 @@
#include "scanner.h"
#include <QtConcurrent>
#include <QMetaObject>
#include <QElapsedTimer>
#include <QDebug>
#include <cstring>
#include <cmath>
#include <algorithm>
namespace rcx {
// ── Pattern parsing ──
static int hexVal(QChar c) {
ushort u = c.unicode();
if (u >= '0' && u <= '9') return u - '0';
if (u >= 'a' && u <= 'f') return u - 'a' + 10;
if (u >= 'A' && u <= 'F') return u - 'A' + 10;
return -1;
}
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
QString* errorMsg)
{
pattern.clear();
mask.clear();
QString trimmed = input.trimmed();
if (trimmed.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern");
return false;
}
// Check for C-style: \xAB\xCD
if (trimmed.startsWith(QStringLiteral("\\x"))) {
QStringList parts = trimmed.split(QStringLiteral("\\x"), Qt::SkipEmptyParts);
for (const QString& part : parts) {
if (part.size() != 2) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid C-style byte: \\x%1").arg(part);
return false;
}
int hi = hexVal(part[0]);
int lo = hexVal(part[1]);
if (hi < 0 || lo < 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex char in: \\x%1").arg(part);
return false;
}
pattern.append(char((hi << 4) | lo));
mask.append(char(0xFF));
}
return !pattern.isEmpty();
}
// Space-separated or packed hex
bool hasSpaces = trimmed.contains(' ');
if (hasSpaces) {
QStringList tokens = trimmed.split(' ', Qt::SkipEmptyParts);
for (const QString& tok : tokens) {
if (tok == QStringLiteral("??") || tok == QStringLiteral("?")) {
pattern.append(char(0));
mask.append(char(0));
} else if (tok.size() == 2) {
int hi = hexVal(tok[0]);
int lo = hexVal(tok[1]);
if (hi < 0 || lo < 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex byte: %1").arg(tok);
return false;
}
pattern.append(char((hi << 4) | lo));
mask.append(char(0xFF));
} else {
if (errorMsg) *errorMsg = QStringLiteral("Invalid token: %1 (expected 2 hex chars or wildcards)").arg(tok);
return false;
}
}
} else {
// Packed: "488B??05"
if (trimmed.size() % 2 != 0) {
if (errorMsg) *errorMsg = QStringLiteral("Odd number of characters in packed pattern");
return false;
}
for (int i = 0; i < trimmed.size(); i += 2) {
QChar c0 = trimmed[i], c1 = trimmed[i + 1];
if ((c0 == '?' && c1 == '?')) {
pattern.append(char(0));
mask.append(char(0));
} else {
int hi = hexVal(c0);
int lo = hexVal(c1);
if (hi < 0 || lo < 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex chars at position %1: %2%3")
.arg(i).arg(c0).arg(c1);
return false;
}
pattern.append(char((hi << 4) | lo));
mask.append(char(0xFF));
}
}
}
if (pattern.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern after parsing");
return false;
}
return true;
}
// ── Value serialization ──
template<typename T>
static void appendLE(QByteArray& out, T val) {
out.append(reinterpret_cast<const char*>(&val), sizeof(T));
}
bool serializeValue(ValueType type, const QString& input,
QByteArray& pattern, QByteArray& mask,
QString* errorMsg)
{
pattern.clear();
mask.clear();
QString trimmed = input.trimmed();
if (trimmed.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty value");
return false;
}
bool ok = false;
switch (type) {
case ValueType::Int8: {
int v = trimmed.toInt(&ok);
if (!ok || v < -128 || v > 127) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid int8 value");
return false;
}
appendLE<int8_t>(pattern, (int8_t)v);
break;
}
case ValueType::Int16: {
int v = trimmed.toInt(&ok);
if (!ok || v < -32768 || v > 32767) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid int16 value");
return false;
}
appendLE<int16_t>(pattern, (int16_t)v);
break;
}
case ValueType::Int32: {
int v = trimmed.toInt(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid int32 value");
return false;
}
appendLE<int32_t>(pattern, (int32_t)v);
break;
}
case ValueType::Int64: {
qlonglong v = trimmed.toLongLong(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid int64 value");
return false;
}
appendLE<int64_t>(pattern, (int64_t)v);
break;
}
case ValueType::UInt8: {
uint v = trimmed.toUInt(&ok);
if (!ok || v > 255) {
// Try hex
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
v = trimmed.toUInt(&ok, 16);
if (!ok || v > 255) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint8 value");
return false;
}
}
appendLE<uint8_t>(pattern, (uint8_t)v);
break;
}
case ValueType::UInt16: {
uint v = trimmed.toUInt(&ok);
if (!ok || v > 65535) {
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
v = trimmed.toUInt(&ok, 16);
if (!ok || v > 65535) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint16 value");
return false;
}
}
appendLE<uint16_t>(pattern, (uint16_t)v);
break;
}
case ValueType::UInt32: {
quint32 v = trimmed.toULong(&ok);
if (!ok) {
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
v = trimmed.toULong(&ok, 16);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint32 value");
return false;
}
}
appendLE<uint32_t>(pattern, v);
break;
}
case ValueType::UInt64: {
quint64 v = trimmed.toULongLong(&ok);
if (!ok) {
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
v = trimmed.toULongLong(&ok, 16);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint64 value");
return false;
}
}
appendLE<uint64_t>(pattern, v);
break;
}
case ValueType::Float: {
float v = trimmed.toFloat(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid float value");
return false;
}
appendLE<float>(pattern, v);
break;
}
case ValueType::Double: {
double v = trimmed.toDouble(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid double value");
return false;
}
appendLE<double>(pattern, v);
break;
}
case ValueType::Vec2: {
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() != 2) {
if (errorMsg) *errorMsg = QStringLiteral("Vec2 requires 2 space-separated floats");
return false;
}
for (const QString& p : parts) {
float v = p.toFloat(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec2: %1").arg(p);
return false;
}
appendLE<float>(pattern, v);
}
break;
}
case ValueType::Vec3: {
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() != 3) {
if (errorMsg) *errorMsg = QStringLiteral("Vec3 requires 3 space-separated floats");
return false;
}
for (const QString& p : parts) {
float v = p.toFloat(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec3: %1").arg(p);
return false;
}
appendLE<float>(pattern, v);
}
break;
}
case ValueType::Vec4: {
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() != 4) {
if (errorMsg) *errorMsg = QStringLiteral("Vec4 requires 4 space-separated floats");
return false;
}
for (const QString& p : parts) {
float v = p.toFloat(&ok);
if (!ok) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec4: %1").arg(p);
return false;
}
appendLE<float>(pattern, v);
}
break;
}
case ValueType::UTF8: {
QByteArray encoded = trimmed.toUtf8();
if (encoded.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-8 string");
return false;
}
pattern = encoded;
break;
}
case ValueType::UTF16: {
// UTF-16LE encoding
for (int i = 0; i < trimmed.size(); i++) {
ushort u = trimmed[i].unicode();
appendLE<uint16_t>(pattern, u);
}
if (pattern.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-16 string");
return false;
}
break;
}
case ValueType::HexBytes: {
// Parse hex bytes (like signature but no wildcards)
QByteArray dummyMask;
if (!parseSignature(trimmed, pattern, dummyMask, errorMsg))
return false;
// HexBytes = exact match, no wildcards
break;
}
}
// Set mask to all 0xFF (exact match) for value scans
mask.fill(char(0xFF), pattern.size());
return true;
}
int naturalAlignment(ValueType type) {
switch (type) {
case ValueType::Int8:
case ValueType::UInt8:
case ValueType::UTF8:
case ValueType::HexBytes:
return 1;
case ValueType::Int16:
case ValueType::UInt16:
case ValueType::UTF16:
return 2;
case ValueType::Int32:
case ValueType::UInt32:
case ValueType::Float:
case ValueType::Vec2:
case ValueType::Vec3:
case ValueType::Vec4:
return 4;
case ValueType::Int64:
case ValueType::UInt64:
case ValueType::Double:
return 8;
}
return 1;
}
// ── Scan engine ──
ScanEngine::ScanEngine(QObject* parent)
: QObject(parent)
{
qRegisterMetaType<QVector<ScanResult>>("QVector<rcx::ScanResult>");
}
bool ScanEngine::isRunning() const {
return m_watcher && m_watcher->isRunning();
}
void ScanEngine::abort() {
m_abort.store(true);
}
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
if (isRunning()) return;
if (req.pattern.isEmpty()) {
emit error(QStringLiteral("Empty pattern"));
return;
}
if (req.pattern.size() != req.mask.size()) {
emit error(QStringLiteral("Pattern and mask size mismatch"));
return;
}
m_abort.store(false);
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
m_watcher = watcher;
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
auto results = watcher->result();
watcher->deleteLater();
if (m_watcher == watcher)
m_watcher = nullptr;
emit finished(results);
});
watcher->setFuture(QtConcurrent::run([this, provider, req]() {
return runScan(provider, req);
}));
}
QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
const ScanRequest& req)
{
QElapsedTimer timer;
timer.start();
QVector<ScanResult> results;
if (!prov || req.pattern.isEmpty())
return results;
auto regions = prov->enumerateRegions();
qDebug() << "[scan] regions:" << regions.size()
<< " pattern:" << req.pattern.size() << "bytes"
<< " align:" << req.alignment
<< " filterExec:" << req.filterExecutable
<< " filterWrite:" << req.filterWritable;
// Fallback for providers that don't enumerate regions (file/buffer)
if (regions.isEmpty()) {
MemoryRegion fallback;
fallback.base = 0;
fallback.size = (uint64_t)prov->size();
fallback.readable = true;
fallback.writable = true;
fallback.executable = false;
regions.append(fallback);
}
const int patternLen = req.pattern.size();
const char* pat = req.pattern.constData();
const char* msk = req.mask.constData();
const int alignment = qMax(1, req.alignment);
// 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;
}
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
if (totalBytes == 0) return results;
uint64_t scannedBytes = 0;
int lastPct = -1;
constexpr int kChunk = 256 * 1024;
for (const auto& region : regions) {
if (m_abort.load()) break;
if (req.filterExecutable && !region.executable) continue;
if (req.filterWritable && !region.writable) continue;
if ((uint64_t)patternLen > region.size) {
scannedBytes += region.size;
continue;
}
const int overlap = patternLen - 1;
QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized);
for (uint64_t off = 0; off < region.size; ) {
if (m_abort.load()) break;
uint64_t remaining = region.size - off;
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
if (!prov->read(region.base + off, chunk.data(), readLen)) {
// Skip unreadable chunk
off += readLen;
scannedBytes += readLen;
continue;
}
int scanEnd = readLen - patternLen;
const char* data = chunk.constData();
for (int i = 0; i <= scanEnd; i += alignment) {
bool match = true;
for (int j = 0; j < patternLen; j++) {
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
match = false;
break;
}
}
if (match) {
ScanResult r;
r.address = region.base + off + (uint64_t)i;
r.regionModule = region.moduleName;
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
results.append(r);
if (results.size() >= req.maxResults)
goto done;
}
}
// Advance with overlap to catch patterns that straddle chunks
uint64_t advance;
if (readLen > overlap)
advance = (uint64_t)(readLen - overlap);
else
advance = 1; // prevent infinite loop on tiny regions
scannedBytes += advance;
off += advance;
// Throttled progress
int pct = (int)(scannedBytes * 100 / totalBytes);
if (pct > 100) pct = 100;
if (pct != lastPct) {
lastPct = pct;
QMetaObject::invokeMethod(this, "progress",
Qt::QueuedConnection, Q_ARG(int, pct));
}
}
}
done:
qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
<< " scanned:" << (scannedBytes / 1024) << "KB";
return results;
}
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask) {
if (isRunning()) return;
m_abort.store(false);
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
m_watcher = watcher;
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
auto results = watcher->result();
watcher->deleteLater();
if (m_watcher == watcher)
m_watcher = nullptr;
emit rescanFinished(results);
});
watcher->setFuture(QtConcurrent::run(
[this, provider, results = std::move(results), readSize,
filterPattern, filterMask]() mutable {
return runRescan(provider, std::move(results), readSize,
filterPattern, filterMask);
}));
}
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask) {
QElapsedTimer timer;
timer.start();
int total = results.size();
if (total == 0 || !prov) return results;
bool hasFilter = !filterPattern.isEmpty();
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
<< "filter:" << (hasFilter ? "yes" : "no");
// Save previous values
for (auto& r : results)
r.previousValue = r.scanValue;
// Sort indices by address for sequential chunked reads
QVector<int> order(total);
for (int i = 0; i < total; i++) order[i] = i;
std::sort(order.begin(), order.end(), [&results](int a, int b) {
return results[a].address < results[b].address;
});
constexpr int kChunk = 256 * 1024;
int updated = 0;
int lastPct = -1;
int chunks = 0;
uint64_t totalBytesRead = 0;
int i = 0;
// Track which results matched the filter (by original index)
QVector<bool> matched(total, !hasFilter); // if no filter, all match
while (i < total && !m_abort.load()) {
uint64_t spanBase = results[order[i]].address;
int spanEnd = i;
// Extend span while next result fits in the same chunk
while (spanEnd + 1 < total) {
uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
if (endAddr - spanBase > (uint64_t)kChunk) break;
spanEnd++;
}
uint64_t spanLast = results[order[spanEnd]].address;
int chunkLen = (int)(spanLast + readSize - spanBase);
QByteArray chunk(chunkLen, '\0');
prov->read(spanBase, chunk.data(), chunkLen);
for (int j = i; j <= spanEnd; j++) {
int idx = order[j];
auto& r = results[idx];
int off = (int)(r.address - spanBase);
r.scanValue = chunk.mid(off, readSize);
// Apply filter: compare re-read bytes against the new pattern
if (hasFilter) {
int patLen = filterPattern.size();
if (r.scanValue.size() >= patLen) {
bool ok = true;
const char* data = r.scanValue.constData();
const char* pat = filterPattern.constData();
const char* msk = filterMask.constData();
for (int k = 0; k < patLen; k++) {
if ((data[k] & msk[k]) != (pat[k] & msk[k])) {
ok = false;
break;
}
}
matched[idx] = ok;
}
}
}
chunks++;
totalBytesRead += chunkLen;
updated += (spanEnd - i + 1);
i = spanEnd + 1;
int pct = updated * 100 / total;
if (pct != lastPct) {
lastPct = pct;
QMetaObject::invokeMethod(this, "progress",
Qt::QueuedConnection, Q_ARG(int, pct));
}
}
// Filter out non-matching results
if (hasFilter) {
QVector<ScanResult> filtered;
filtered.reserve(total);
for (int k = 0; k < total; k++) {
if (matched[k])
filtered.append(std::move(results[k]));
}
qDebug() << "[rescan] done:" << filtered.size() << "/" << total
<< "matched in" << timer.elapsed() << "ms |" << chunks
<< "chunks," << (totalBytesRead / 1024) << "KB read";
return filtered;
}
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
<< timer.elapsed() << "ms |" << chunks << "chunks,"
<< (totalBytesRead / 1024) << "KB read";
return results;
}
} // namespace rcx

94
src/scanner.h Normal file
View File

@@ -0,0 +1,94 @@
#pragma once
#include "providers/provider.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QVector>
#include <QFutureWatcher>
#include <atomic>
#include <memory>
namespace rcx {
// ── Scan request / result ──
struct ScanRequest {
QByteArray pattern; // literal bytes to match
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
bool filterExecutable = false; // only scan +x regions
bool filterWritable = false; // only scan +w regions
int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword
int maxResults = 50000;
};
struct ScanResult {
uint64_t address;
QString regionModule;
QByteArray scanValue; // cached bytes at scan/update time
QByteArray previousValue; // value before last update
};
// ── Value scan types ──
enum class ValueType {
Int8, Int16, Int32, Int64,
UInt8, UInt16, UInt32, UInt64,
Float, Double,
Vec2, Vec3, Vec4,
UTF8, UTF16,
HexBytes
};
// ── Pattern parsing ──
// Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask.
// Returns true on success. On failure, sets errorMsg.
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
QString* errorMsg = nullptr);
// Serialize a typed value into raw bytes for exact-match scanning.
// Returns true on success. On failure, sets errorMsg.
bool serializeValue(ValueType type, const QString& input,
QByteArray& pattern, QByteArray& mask,
QString* errorMsg = nullptr);
// Natural alignment for a value type (used as default alignment for value scans).
int naturalAlignment(ValueType type);
// ── Scan engine ──
class ScanEngine : public QObject {
Q_OBJECT
public:
explicit ScanEngine(QObject* parent = nullptr);
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
void startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern = {},
const QByteArray& filterMask = {});
void abort();
bool isRunning() const;
signals:
void progress(int percent);
void finished(QVector<ScanResult> results);
void rescanFinished(QVector<ScanResult> results);
void error(QString message);
private:
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask);
std::atomic<bool> m_abort{false};
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
};
} // namespace rcx
Q_DECLARE_METATYPE(QVector<rcx::ScanResult>)

745
src/scannerpanel.cpp Normal file
View File

@@ -0,0 +1,745 @@
#include "scannerpanel.h"
#include "addressparser.h"
#include <cstring>
#include <QElapsedTimer>
#include <QDebug>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QClipboard>
#include <QApplication>
#include <QMenu>
#include <QPainter>
namespace rcx {
void AddressDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const {
// Draw background (selection/hover handled by style)
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
opt.text.clear();
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
QString text = index.data(Qt::DisplayRole).toString();
if (text.isEmpty()) return;
// Find first non-zero hex digit (skip backtick)
int dimEnd = 0;
for (int i = 0; i < text.size(); i++) {
QChar c = text[i];
if (c == '`') { dimEnd = i + 1; continue; }
if (c != '0') break;
dimEnd = i + 1;
}
QRect textRect = opt.rect.adjusted(7, 0, -4, 0); // match item padding
painter->setFont(opt.font);
if (dimEnd > 0) {
painter->setPen(dimColor);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.left(dimEnd));
// Advance past dim prefix
int dimWidth = painter->fontMetrics().horizontalAdvance(text.left(dimEnd));
textRect.setLeft(textRect.left() + dimWidth);
}
painter->setPen(brightColor);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.mid(dimEnd));
}
ScannerPanel::ScannerPanel(QWidget* parent)
: QWidget(parent)
, m_engine(new ScanEngine(this))
{
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(6, 6, 6, 6);
mainLayout->setSpacing(4);
// ── Row 1: Mode + pattern/value input ──
auto* inputRow = new QHBoxLayout;
inputRow->setSpacing(6);
m_modeCombo = new QComboBox(this);
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/regex.svg")),
QStringLiteral("Signature"));
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/symbol-variable.svg")),
QStringLiteral("Value"));
updateComboWidth();
inputRow->addWidget(m_modeCombo);
// Signature input
m_patternLabel = new QLabel(QStringLiteral("Pattern:"), this);
inputRow->addWidget(m_patternLabel);
m_patternEdit = new QLineEdit(this);
m_patternEdit->setPlaceholderText(QStringLiteral("48 8B ?? 05 ?? ?? ?? ?? CC"));
inputRow->addWidget(m_patternEdit, 1);
// Value input (hidden initially)
m_typeLabel = new QLabel(QStringLiteral("Type:"), this);
inputRow->addWidget(m_typeLabel);
m_typeCombo = new QComboBox(this);
m_typeCombo->addItem(QStringLiteral("int8"), (int)ValueType::Int8);
m_typeCombo->addItem(QStringLiteral("int16"), (int)ValueType::Int16);
m_typeCombo->addItem(QStringLiteral("int32"), (int)ValueType::Int32);
m_typeCombo->addItem(QStringLiteral("int64"), (int)ValueType::Int64);
m_typeCombo->addItem(QStringLiteral("uint8"), (int)ValueType::UInt8);
m_typeCombo->addItem(QStringLiteral("uint16"), (int)ValueType::UInt16);
m_typeCombo->addItem(QStringLiteral("uint32"), (int)ValueType::UInt32);
m_typeCombo->addItem(QStringLiteral("uint64"), (int)ValueType::UInt64);
m_typeCombo->addItem(QStringLiteral("float"), (int)ValueType::Float);
m_typeCombo->addItem(QStringLiteral("double"), (int)ValueType::Double);
m_typeCombo->setCurrentIndex(2); // default: int32
inputRow->addWidget(m_typeCombo);
m_valueLabel = new QLabel(QStringLiteral("Value:"), this);
inputRow->addWidget(m_valueLabel);
m_valueEdit = new QLineEdit(this);
m_valueEdit->setPlaceholderText(QStringLiteral("12345"));
inputRow->addWidget(m_valueEdit, 1);
mainLayout->addLayout(inputRow);
// ── Row 2: Filters + scan button + progress ──
auto* filterRow = new QHBoxLayout;
filterRow->setSpacing(6);
m_execCheck = new QCheckBox(QStringLiteral("Executable"), this);
filterRow->addWidget(m_execCheck);
m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this);
filterRow->addWidget(m_writeCheck);
filterRow->addStretch();
m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")),
QStringLiteral("Scan"), this);
filterRow->addWidget(m_scanBtn);
m_updateBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/refresh.svg")),
QStringLiteral("Re-scan"), this);
m_updateBtn->setEnabled(false);
filterRow->addWidget(m_updateBtn);
m_progressBar = new QProgressBar(this);
m_progressBar->setRange(0, 100);
m_progressBar->setTextVisible(true);
m_progressBar->setFixedWidth(150);
m_progressBar->hide();
filterRow->addWidget(m_progressBar);
mainLayout->addLayout(filterRow);
// ── Results table ──
m_resultTable = new QTableWidget(this);
m_resultTable->setColumnCount(2);
m_resultTable->horizontalHeader()->hide();
m_resultTable->verticalHeader()->hide();
m_resultTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
m_resultTable->horizontalHeader()->setStretchLastSection(true);
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection);
m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked);
m_resultTable->setShowGrid(false);
m_resultTable->setMouseTracking(true);
m_resultTable->setFocusPolicy(Qt::StrongFocus);
m_resultTable->setContextMenuPolicy(Qt::CustomContextMenu);
// Address column delegate for dimmed leading zeros
m_addrDelegate = new AddressDelegate(this);
m_resultTable->setItemDelegateForColumn(0, m_addrDelegate);
mainLayout->addWidget(m_resultTable, 1);
// ── Row 3: Status + action buttons ──
auto* actionRow = new QHBoxLayout;
actionRow->setSpacing(6);
m_statusLabel = new QLabel(QStringLiteral("Ready"), this);
actionRow->addWidget(m_statusLabel, 1);
m_gotoBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
QStringLiteral("Go to Address"), this);
m_gotoBtn->setEnabled(false);
actionRow->addWidget(m_gotoBtn);
m_copyBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
QStringLiteral("Copy Address"), this);
m_copyBtn->setEnabled(false);
actionRow->addWidget(m_copyBtn);
mainLayout->addLayout(actionRow);
// ── Initial state: signature mode ──
m_typeLabel->hide();
m_typeCombo->hide();
m_valueLabel->hide();
m_valueEdit->hide();
m_execCheck->setChecked(true);
// ── Connections ──
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ScannerPanel::onModeChanged);
connect(m_scanBtn, &QPushButton::clicked,
this, &ScannerPanel::onScanClicked);
connect(m_updateBtn, &QPushButton::clicked,
this, &ScannerPanel::onUpdateClicked);
connect(m_gotoBtn, &QPushButton::clicked,
this, &ScannerPanel::onGoToAddress);
connect(m_copyBtn, &QPushButton::clicked,
this, &ScannerPanel::onCopyAddress);
connect(m_resultTable, &QTableWidget::cellDoubleClicked,
this, &ScannerPanel::onResultDoubleClicked);
connect(m_resultTable, &QTableWidget::cellChanged,
this, &ScannerPanel::onCellEdited);
connect(m_resultTable, &QTableWidget::itemSelectionChanged, this, [this]() {
bool hasSel = !m_resultTable->selectedItems().isEmpty();
m_gotoBtn->setEnabled(hasSel);
m_copyBtn->setEnabled(hasSel);
});
connect(m_resultTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
int row = m_resultTable->rowAt(pos.y());
if (row < 0 || row >= m_results.size()) return;
QMenu menu;
auto* copyAddr = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
QStringLiteral("Copy Address"));
auto* copyVal = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
QStringLiteral("Copy Value"));
auto* goTo = menu.addAction(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
QStringLiteral("Go to Address"));
auto* chosen = menu.exec(m_resultTable->viewport()->mapToGlobal(pos));
if (chosen == copyAddr) {
QString addr = QStringLiteral("0x%1")
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
QApplication::clipboard()->setText(addr);
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
} else if (chosen == copyVal) {
QApplication::clipboard()->setText(formatValue(m_results[row].scanValue));
m_statusLabel->setText(QStringLiteral("Copied value"));
} else if (chosen == goTo) {
emit goToAddress(m_results[row].address);
}
});
connect(m_engine, &ScanEngine::progress, this, [this](int pct) {
m_progressBar->setValue(pct);
});
connect(m_engine, &ScanEngine::finished,
this, &ScannerPanel::onScanFinished);
connect(m_engine, &ScanEngine::rescanFinished,
this, &ScannerPanel::onRescanFinished);
connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) {
m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg));
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
});
}
void ScannerPanel::setProviderGetter(ProviderGetter getter) {
m_providerGetter = std::move(getter);
}
void ScannerPanel::setEditorFont(const QFont& font) {
m_resultTable->setFont(font);
QFontMetrics fm(font);
m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6);
// Address column width: "00000000`00000000" + padding
m_resultTable->setColumnWidth(0, fm.horizontalAdvance(QStringLiteral("00000000`00000000")) + 20);
m_patternEdit->setFont(font);
m_valueEdit->setFont(font);
m_modeCombo->setFont(font);
m_typeCombo->setFont(font);
m_statusLabel->setFont(font);
m_scanBtn->setFont(font);
m_gotoBtn->setFont(font);
m_copyBtn->setFont(font);
m_patternLabel->setFont(font);
m_typeLabel->setFont(font);
m_valueLabel->setFont(font);
m_execCheck->setFont(font);
m_writeCheck->setFont(font);
m_updateBtn->setFont(font);
updateComboWidth();
}
void ScannerPanel::updateComboWidth() {
QFontMetrics fm(m_modeCombo->font());
int maxW = 0;
for (int i = 0; i < m_modeCombo->count(); i++)
maxW = qMax(maxW, fm.horizontalAdvance(m_modeCombo->itemText(i)));
m_modeCombo->setFixedWidth(maxW + 50); // icon + dropdown arrow + padding
}
void ScannerPanel::onModeChanged(int index) {
bool isSig = (index == 0);
m_patternLabel->setVisible(isSig);
m_patternEdit->setVisible(isSig);
m_typeLabel->setVisible(!isSig);
m_typeCombo->setVisible(!isSig);
m_valueLabel->setVisible(!isSig);
m_valueEdit->setVisible(!isSig);
// Auto-toggle filters: signatures → executable code, values → writable data
m_execCheck->setChecked(isSig);
m_writeCheck->setChecked(!isSig);
}
void ScannerPanel::onScanClicked() {
if (m_engine->isRunning()) {
m_engine->abort();
return; // finished/rescanFinished handler resets UI
}
// Get provider
std::shared_ptr<Provider> provider;
if (m_providerGetter)
provider = m_providerGetter();
if (!provider) {
m_statusLabel->setText(QStringLiteral("No source attached"));
return;
}
// Build request
ScanRequest req = buildRequest();
if (req.pattern.isEmpty())
return; // error already shown by buildRequest
m_lastScanMode = m_modeCombo->currentIndex();
if (m_lastScanMode == 1)
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
m_lastPattern = req.pattern;
m_scanBtn->setText(QStringLiteral("Cancel"));
m_progressBar->setValue(0);
m_progressBar->show();
m_statusLabel->setText(QStringLiteral("Scanning..."));
m_engine->start(provider, req);
}
ScanRequest ScannerPanel::buildRequest() {
ScanRequest req;
QString err;
if (m_modeCombo->currentIndex() == 0) {
// Signature mode
if (!parseSignature(m_patternEdit->text(), req.pattern, req.mask, &err)) {
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
return {};
}
req.alignment = 1;
} else {
// Value mode
auto vt = (ValueType)m_typeCombo->currentData().toInt();
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
return {};
}
req.alignment = naturalAlignment(vt);
}
req.filterExecutable = m_execCheck->isChecked();
req.filterWritable = m_writeCheck->isChecked();
return req;
}
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
m_results = std::move(results);
// Bytes are cached by the engine during scan.
// Value mode: override with exact search pattern (engine caches raw chunk bytes).
for (auto& r : m_results) {
r.previousValue.clear();
if (m_lastScanMode == 1)
r.scanValue = m_lastPattern;
}
m_updateBtn->setEnabled(!m_results.isEmpty());
{
QElapsedTimer pt;
pt.start();
populateTable(false);
qDebug() << "[panel] populateTable(initial):" << m_results.size()
<< "results," << pt.elapsed() << "ms";
}
int n = m_results.size();
m_statusLabel->setText(QStringLiteral("%1 result%2")
.arg(n).arg(n == 1 ? "" : "s"));
}
void ScannerPanel::populateTable(bool showPrevious) {
constexpr int kMaxRows = 10000;
m_resultTable->blockSignals(true);
int cols = showPrevious ? 3 : 2;
m_resultTable->setColumnCount(cols);
int displayCount = qMin(m_results.size(), kMaxRows);
m_resultTable->setRowCount(displayCount);
for (int i = 0; i < displayCount; i++) {
const auto& r = m_results[i];
// Address column — WinDbg backtick format: 00000000`00000000
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
hexPart.insert(8, '`');
auto* addrItem = new QTableWidgetItem(hexPart);
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
m_resultTable->setItem(i, 0, addrItem);
// Value column
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
m_resultTable->setItem(i, 1, valItem);
// Previous column
if (showPrevious) {
auto* prevItem = new QTableWidgetItem(
r.previousValue.isEmpty() ? QString() : formatValue(r.previousValue));
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
m_resultTable->setItem(i, 2, prevItem);
}
}
m_resultTable->blockSignals(false);
}
void ScannerPanel::onUpdateClicked() {
if (m_results.isEmpty() || m_engine->isRunning()) return;
std::shared_ptr<Provider> prov;
if (m_providerGetter)
prov = m_providerGetter();
if (!prov) {
m_statusLabel->setText(QStringLiteral("No source attached"));
return;
}
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
// Build filter from current input field
QByteArray filterPattern, filterMask;
if (m_lastScanMode == 0) {
// Signature mode
QString err;
if (!m_patternEdit->text().trimmed().isEmpty()) {
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
return;
}
}
} else {
// Value mode
QString err;
if (!m_valueEdit->text().trimmed().isEmpty()) {
auto vt = (ValueType)m_typeCombo->currentData().toInt();
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
return;
}
m_lastValueType = vt;
}
}
// Update last pattern so display uses the new value
if (!filterPattern.isEmpty())
m_lastPattern = filterPattern;
m_preRescanCount = m_results.size();
m_updateBtn->setEnabled(false);
m_scanBtn->setText(QStringLiteral("Cancel"));
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
m_progressBar->setValue(0);
m_progressBar->show();
m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask);
}
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();
m_results = std::move(results);
m_updateBtn->setEnabled(!m_results.isEmpty());
{
QElapsedTimer pt;
pt.start();
populateTable(true);
qDebug() << "[panel] populateTable(rescan):" << m_results.size()
<< "results," << pt.elapsed() << "ms";
}
int n = m_results.size();
if (m_preRescanCount > 0 && n < m_preRescanCount)
m_statusLabel->setText(QStringLiteral("%1 of %2 results match")
.arg(n).arg(m_preRescanCount));
else
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
.arg(n).arg(n == 1 ? "" : "s"));
}
void ScannerPanel::onGoToAddress() {
int row = m_resultTable->currentRow();
if (row < 0 || row >= m_results.size()) return;
emit goToAddress(m_results[row].address);
}
void ScannerPanel::onCopyAddress() {
int row = m_resultTable->currentRow();
if (row < 0 || row >= m_results.size()) return;
QString addr = QStringLiteral("0x%1")
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
QApplication::clipboard()->setText(addr);
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
}
void ScannerPanel::onResultDoubleClicked(int row, int col) {
// Double-click on address column navigates (editing also starts via edit trigger)
// Double-click on preview column only starts inline editing
Q_UNUSED(col);
Q_UNUSED(row);
// Navigation is handled by Go to Address button or onCellEdited for address expressions
}
void ScannerPanel::onCellEdited(int row, int col) {
if (row < 0 || row >= m_results.size()) return;
auto* item = m_resultTable->item(row, col);
if (!item) return;
QString text = item->text().trimmed();
if (col == 0) {
// Address column — evaluate expression via AddressParser
AddressParserCallbacks cbs;
std::shared_ptr<Provider> prov;
if (m_providerGetter)
prov = m_providerGetter();
if (prov) {
auto* p = prov.get();
cbs.resolveModule = [p](const QString& name, bool* ok) -> uint64_t {
uint64_t base = p->symbolToAddress(name);
*ok = (base != 0);
return base;
};
int ptrSz = p->pointerSize();
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = p->read(addr, &val, ptrSz);
return val;
};
}
int evalPtrSize = prov ? prov->pointerSize() : 8;
auto result = AddressParser::evaluate(text, evalPtrSize, &cbs);
if (result.ok) {
m_results[row].address = result.value;
emit goToAddress(result.value);
// Reformat the address cell
m_resultTable->blockSignals(true);
QString hexPart = QStringLiteral("%1").arg(result.value, 16, 16, QLatin1Char('0')).toUpper();
hexPart.insert(8, '`');
item->setText(hexPart);
// Re-read preview at new address and update cache
if (prov) {
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
m_results[row].scanValue = prov->readBytes(result.value, readSize);
if (auto* prevItem = m_resultTable->item(row, 1))
prevItem->setText(formatValue(m_results[row].scanValue));
}
m_resultTable->blockSignals(false);
} else {
m_statusLabel->setText(QStringLiteral("Expression error: %1").arg(result.error));
// Restore original address
m_resultTable->blockSignals(true);
QString hexPart = QStringLiteral("%1").arg(m_results[row].address, 16, 16, QLatin1Char('0')).toUpper();
hexPart.insert(8, '`');
item->setText(hexPart);
m_resultTable->blockSignals(false);
}
} else if (col == 1) {
// Preview column — parse hex bytes and write to provider
std::shared_ptr<Provider> prov;
if (m_providerGetter)
prov = m_providerGetter();
if (!prov || !prov->isWritable()) {
m_statusLabel->setText(QStringLiteral("Provider is read-only"));
return;
}
QByteArray bytes;
uint64_t addr = m_results[row].address;
if (m_lastScanMode == 0) {
// Signature mode — parse space-separated hex bytes
QStringList tokens = text.split(' ', Qt::SkipEmptyParts);
for (const QString& tok : tokens) {
bool ok;
uint val = tok.toUInt(&ok, 16);
if (!ok || val > 0xFF) {
m_statusLabel->setText(QStringLiteral("Invalid hex byte: %1").arg(tok));
return;
}
bytes.append(char(val));
}
} else {
// Value mode — parse native type
bool ok = false;
bytes.resize(valueSize());
char* d = bytes.data();
switch (m_lastValueType) {
case ValueType::Int8: { auto v = (int8_t)text.toInt(&ok); if (ok) memcpy(d, &v, 1); break; }
case ValueType::UInt8: { auto v = (uint8_t)text.toUInt(&ok); if (ok) memcpy(d, &v, 1); break; }
case ValueType::Int16: { auto v = (int16_t)text.toShort(&ok); if (ok) memcpy(d, &v, 2); break; }
case ValueType::UInt16: { auto v = text.toUShort(&ok); if (ok) memcpy(d, &v, 2); break; }
case ValueType::Int32: { auto v = text.toInt(&ok); if (ok) memcpy(d, &v, 4); break; }
case ValueType::UInt32: { auto v = text.toUInt(&ok); if (ok) memcpy(d, &v, 4); break; }
case ValueType::Int64: { auto v = text.toLongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
case ValueType::UInt64: { auto v = text.toULongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
case ValueType::Float: { auto v = text.toFloat(&ok); if (ok) memcpy(d, &v, 4); break; }
case ValueType::Double: { auto v = text.toDouble(&ok); if (ok) memcpy(d, &v, 8); break; }
default: break;
}
if (!ok) {
m_statusLabel->setText(QStringLiteral("Invalid value"));
return;
}
}
if (bytes.isEmpty()) return;
if (prov->writeBytes(addr, bytes)) {
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
.arg(bytes.size())
.arg(bytes.size() == 1 ? "" : "s")
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
// Re-read and update cache
m_resultTable->blockSignals(true);
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
m_results[row].scanValue = prov->readBytes(addr, readSize);
item->setText(formatValue(m_results[row].scanValue));
m_resultTable->blockSignals(false);
} else {
m_statusLabel->setText(QStringLiteral("Write failed"));
}
}
}
void ScannerPanel::applyTheme(const Theme& theme) {
// Address delegate colors
m_addrDelegate->dimColor = theme.textFaint;
m_addrDelegate->brightColor = theme.text;
// Results table — editor-matching style
m_resultTable->setStyleSheet(QStringLiteral(
"QTableWidget { background: %1; color: %2; border: none; }"
"QTableWidget::item { padding: 2px 6px; border: none; }"
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }"
"QTableWidget QLineEdit { background: %1; color: %2; border: 1px solid %4;"
" padding: 1px 4px; selection-background-color: %5; }")
.arg(theme.background.name(), theme.text.name(), theme.hover.name(),
theme.borderFocused.name(), theme.selection.name()));
// Input fields
QString lineEditStyle = QStringLiteral(
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
"QLineEdit:focus { border-color: %4; }")
.arg(theme.background.name(), theme.text.name(),
theme.border.name(), theme.borderFocused.name());
m_patternEdit->setStyleSheet(lineEditStyle);
m_valueEdit->setStyleSheet(lineEditStyle);
// Combo boxes
QString comboStyle = QStringLiteral(
"QComboBox { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px 2px 4px; }"
"QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right;"
" width: 16px; border-left: 1px solid %3; }"
"QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg); width: 10px; height: 10px; }"
"QComboBox QAbstractItemView { background: %1; color: %2; selection-background-color: %4; }")
.arg(theme.background.name(), theme.text.name(),
theme.border.name(), theme.hover.name());
m_modeCombo->setStyleSheet(comboStyle);
m_typeCombo->setStyleSheet(comboStyle);
// Labels
QPalette lp;
lp.setColor(QPalette::WindowText, theme.textDim);
m_patternLabel->setPalette(lp);
m_typeLabel->setPalette(lp);
m_valueLabel->setPalette(lp);
m_statusLabel->setPalette(lp);
// Checkboxes
QPalette cp;
cp.setColor(QPalette::WindowText, theme.textDim);
m_execCheck->setPalette(cp);
m_writeCheck->setPalette(cp);
// Buttons
QString btnStyle = QStringLiteral(
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
"QPushButton:hover { background: %4; }"
"QPushButton:pressed { background: %5; }"
"QPushButton:disabled { color: %6; }")
.arg(theme.button.name(), theme.text.name(), theme.border.name(),
theme.hover.name(), theme.hover.darker(130).name(),
theme.textMuted.name());
m_scanBtn->setStyleSheet(btnStyle);
m_updateBtn->setStyleSheet(btnStyle);
m_gotoBtn->setStyleSheet(btnStyle);
m_copyBtn->setStyleSheet(btnStyle);
// Progress bar
m_progressBar->setStyleSheet(QStringLiteral(
"QProgressBar { background: %1; border: 1px solid %2; text-align: center; color: %3; }"
"QProgressBar::chunk { background: %4; }")
.arg(theme.background.name(), theme.border.name(),
theme.textDim.name(), theme.indHoverSpan.name()));
}
int ScannerPanel::valueSize() const {
switch (m_lastValueType) {
case ValueType::Int8: case ValueType::UInt8: return 1;
case ValueType::Int16: case ValueType::UInt16: return 2;
case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4;
case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8;
default: return 16;
}
}
QString ScannerPanel::formatValue(const QByteArray& bytes) const {
if (m_lastScanMode == 0) {
// Signature mode — hex bytes
QString s;
for (int j = 0; j < bytes.size(); j++) {
if (j > 0) s += ' ';
s += QStringLiteral("%1").arg((uint8_t)bytes[j], 2, 16, QLatin1Char('0')).toUpper();
}
return s;
}
// Value mode — native type
const char* d = bytes.constData();
int sz = bytes.size();
switch (m_lastValueType) {
case ValueType::Int8: if (sz >= 1) return QString::number((int8_t)d[0]); break;
case ValueType::UInt8: if (sz >= 1) return QString::number((uint8_t)d[0]); break;
case ValueType::Int16: if (sz >= 2) { int16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
case ValueType::UInt16: if (sz >= 2) { uint16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
case ValueType::Int32: if (sz >= 4) { int32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
case ValueType::UInt32: if (sz >= 4) { uint32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
case ValueType::Int64: if (sz >= 8) { int64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
case ValueType::UInt64: if (sz >= 8) { uint64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
case ValueType::Float: if (sz >= 4) { float v; memcpy(&v, d, 4); return QString::number(v, 'g', 9); } break;
case ValueType::Double: if (sz >= 8) { double v; memcpy(&v, d, 8); return QString::number(v, 'g', 17); } break;
default: break;
}
return QStringLiteral("??");
}
} // namespace rcx

113
src/scannerpanel.h Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include "scanner.h"
#include "themes/theme.h"
#include <QWidget>
#include <QComboBox>
#include <QLineEdit>
#include <QCheckBox>
#include <QPushButton>
#include <QProgressBar>
#include <QTableWidget>
#include <QStyledItemDelegate>
#include <QLabel>
#include <functional>
#include <memory>
namespace rcx {
// Delegate that paints address with dimmed high-bytes prefix
class AddressDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
QColor dimColor;
QColor brightColor;
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
};
class ScannerPanel : public QWidget {
Q_OBJECT
public:
explicit ScannerPanel(QWidget* parent = nullptr);
using ProviderGetter = std::function<std::shared_ptr<Provider>()>;
void setProviderGetter(ProviderGetter getter);
void setEditorFont(const QFont& font);
void applyTheme(const Theme& theme);
// Test accessors
QComboBox* modeCombo() const { return m_modeCombo; }
QLineEdit* patternEdit() const { return m_patternEdit; }
QComboBox* typeCombo() const { return m_typeCombo; }
QLineEdit* valueEdit() const { return m_valueEdit; }
QCheckBox* execCheck() const { return m_execCheck; }
QCheckBox* writeCheck() const { return m_writeCheck; }
QPushButton* scanButton() const { return m_scanBtn; }
QPushButton* updateButton() const { return m_updateBtn; }
QProgressBar* progressBar() const { return m_progressBar; }
QTableWidget* resultsTable() const { return m_resultTable; }
QLabel* statusLabel() const { return m_statusLabel; }
QPushButton* gotoButton() const { return m_gotoBtn; }
QPushButton* copyButton() const { return m_copyBtn; }
ScanEngine* engine() const { return m_engine; }
signals:
void goToAddress(uint64_t address);
private slots:
void onModeChanged(int index);
void onScanClicked();
void onScanFinished(QVector<ScanResult> results);
void onGoToAddress();
void onCopyAddress();
void onResultDoubleClicked(int row, int col);
void onCellEdited(int row, int col);
void onUpdateClicked();
void onRescanFinished(QVector<ScanResult> results);
private:
ScanRequest buildRequest();
void populateTable(bool showPrevious);
void updateComboWidth();
// Input widgets
QComboBox* m_modeCombo; // Signature / Value
QLineEdit* m_patternEdit; // Signature pattern input
QComboBox* m_typeCombo; // Value type dropdown
QLineEdit* m_valueEdit; // Value input
QLabel* m_patternLabel;
QLabel* m_typeLabel;
QLabel* m_valueLabel;
// Filters
QCheckBox* m_execCheck;
QCheckBox* m_writeCheck;
// Actions
QPushButton* m_scanBtn;
QPushButton* m_updateBtn;
QProgressBar* m_progressBar;
// Results
QTableWidget* m_resultTable;
AddressDelegate* m_addrDelegate;
QLabel* m_statusLabel;
QPushButton* m_gotoBtn;
QPushButton* m_copyBtn;
// Engine
ScanEngine* m_engine;
ProviderGetter m_providerGetter;
QVector<ScanResult> m_results;
int m_lastScanMode = 0; // 0=signature, 1=value
ValueType m_lastValueType = ValueType::Int32;
QByteArray m_lastPattern; // serialized search value
int m_preRescanCount = 0; // result count before last rescan
QString formatValue(const QByteArray& bytes) const;
int valueSize() const;
};
} // namespace rcx

View File

@@ -0,0 +1,32 @@
{
"name": "Mid",
"background": "#0D1219",
"backgroundAlt": "#121720",
"surface": "#161C28",
"border": "#1E2636",
"borderFocused": "#485068",
"button": "#181E2C",
"text": "#B0B8CC",
"textDim": "#505C74",
"textMuted": "#384258",
"textFaint": "#2C3448",
"hover": "#121720",
"selected": "#121720",
"selection": "#1A2038",
"syntaxKeyword": "#5688C0",
"syntaxNumber": "#90B480",
"syntaxString": "#B88060",
"syntaxComment": "#385030",
"syntaxPreproc": "#9868A8",
"syntaxType": "#8FDBFE",
"indHoverSpan": "#C09038",
"indCmdPill": "#141A26",
"indDataChanged": "#608C54",
"indHeatCold": "#B09030",
"indHeatWarm": "#C09038",
"indHeatHot": "#C83838",
"indHintGreen": "#385830",
"markerPtr": "#C83838",
"markerCycle": "#B89028",
"markerError": "#481818"
}

View File

@@ -10,8 +10,8 @@
"textDim": "#858585",
"textMuted": "#585858",
"textFaint": "#505050",
"hover": "#2b2b2b",
"selected": "#232323",
"hover": "#2a2a2a",
"selected": "#2a2d2e",
"selection": "#2b2b2b",
"syntaxKeyword": "#569cd6",
"syntaxNumber": "#b5cea8",
@@ -22,6 +22,9 @@
"indHoverSpan": "#E6B450",
"indCmdPill": "#2a2a2a",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#D4A945",
"indHeatWarm": "#E6B450",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",
"markerPtr": "#f44747",
"markerCycle": "#e5a00d",

View File

@@ -10,8 +10,8 @@
"textDim": "#858585",
"textMuted": "#636369",
"textFaint": "#4d4d55",
"hover": "#3e3e42",
"selected": "#2d2d30",
"hover": "#2c2c2f",
"selected": "#262629",
"selection": "#264f78",
"syntaxKeyword": "#569cd6",
"syntaxNumber": "#b5cea8",
@@ -22,6 +22,9 @@
"indHoverSpan": "#b180d7",
"indCmdPill": "#2d2d30",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#D4A945",
"indHeatWarm": "#d69d85",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",
"markerPtr": "#f44747",
"markerCycle": "#e5a00d",

Some files were not shown because too many files have changed in this diff Show More