mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
52 Commits
v2027.02.1
...
snapshot-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c3b4af045 | ||
|
|
5ae9ca0979 | ||
|
|
e064646c02 | ||
|
|
c6c56ffaee | ||
|
|
aba8e5cac9 | ||
|
|
3a5d03fae0 | ||
|
|
df79da54e3 | ||
|
|
e3ff4dfe71 | ||
|
|
735e4ea9f7 | ||
|
|
d937d2f42e | ||
|
|
3685530287 | ||
|
|
9e90f66ca0 | ||
|
|
f53fa84a15 | ||
|
|
13e28e8791 | ||
|
|
079b3121ce | ||
|
|
5e40349768 | ||
|
|
8dd6110ec6 | ||
|
|
eb27fc7988 | ||
|
|
85994d68b9 | ||
|
|
55dc5d5875 | ||
|
|
3a92336132 | ||
|
|
f9b33f2ba7 | ||
|
|
f2dab07870 | ||
|
|
9d22a5ed69 | ||
|
|
193ab81ecf | ||
|
|
aa0840b332 | ||
|
|
f3631f17ff | ||
|
|
42e9bde7ba | ||
|
|
07fedf0ae8 | ||
|
|
2e02a01495 | ||
|
|
71bc51cbab | ||
|
|
60a97ab81b | ||
|
|
bb00e75019 | ||
|
|
c038c59e34 | ||
|
|
862f76b984 | ||
|
|
818285a76e | ||
|
|
ef5e2ebdb9 | ||
|
|
75fedd2222 | ||
|
|
389745e501 | ||
|
|
1473a58742 | ||
|
|
4192a4dad3 | ||
|
|
4c6bb9564f | ||
|
|
0ef9841f90 | ||
|
|
0a8244dad4 | ||
|
|
c856ba2697 | ||
|
|
b44dc9e96b | ||
|
|
0f2ded471f | ||
|
|
c9377c3afd | ||
|
|
a86912add1 | ||
|
|
5a9a6b754f | ||
|
|
0df52e82b8 | ||
|
|
9a342286ee |
191
.github/workflows/build.yml
vendored
Normal file
191
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Qt6
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
version: '6.8.1'
|
||||||
|
arch: 'win64_msvc2022_64'
|
||||||
|
cache: true
|
||||||
|
aqtversion: '==3.1.21'
|
||||||
|
|
||||||
|
- uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
with:
|
||||||
|
arch: x64
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller|test_windbg_provider|test_com_security"
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Reclass-win64-qt6
|
||||||
|
path: |
|
||||||
|
build/Reclass.exe
|
||||||
|
build/ReclassMcpBridge.exe
|
||||||
|
build/Plugins/*.dll
|
||||||
|
build/*.dll
|
||||||
|
build/platforms/
|
||||||
|
build/styles/
|
||||||
|
build/imageformats/
|
||||||
|
build/iconengines/
|
||||||
|
build/themes/
|
||||||
|
build/examples/
|
||||||
|
build/screenshot.png
|
||||||
|
|
||||||
|
- name: Get date tag
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
id: date
|
||||||
|
shell: bash
|
||||||
|
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Package release zip
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
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 release asset
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
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: Reclass-win64-qt6.zip
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
linux:
|
||||||
|
needs: windows
|
||||||
|
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
|
||||||
|
aqtversion: '==3.1.21'
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller"
|
||||||
|
env:
|
||||||
|
QT_QPA_PLATFORM: offscreen
|
||||||
|
|
||||||
|
- 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
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Reclass-linux64-qt6
|
||||||
|
path: Reclass-linux64-qt6.AppImage
|
||||||
|
|
||||||
|
- name: Get date tag
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
id: date
|
||||||
|
shell: bash
|
||||||
|
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload release asset
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
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: Reclass-linux64-qt6.AppImage
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -59,7 +59,17 @@ add_executable(Reclass
|
|||||||
src/themes/thememanager.cpp
|
src/themes/thememanager.cpp
|
||||||
src/themes/themeeditor.h
|
src/themes/themeeditor.h
|
||||||
src/themes/themeeditor.cpp
|
src/themes/themeeditor.cpp
|
||||||
|
src/import_reclass_xml.h
|
||||||
|
src/import_reclass_xml.cpp
|
||||||
|
src/import_source.h
|
||||||
|
src/import_source.cpp
|
||||||
|
src/export_reclass_xml.h
|
||||||
|
src/export_reclass_xml.cpp
|
||||||
src/mainwindow.h
|
src/mainwindow.h
|
||||||
|
src/optionsdialog.h
|
||||||
|
src/optionsdialog.cpp
|
||||||
|
src/titlebar.h
|
||||||
|
src/titlebar.cpp
|
||||||
src/mcp/mcp_bridge.h
|
src/mcp/mcp_bridge.h
|
||||||
src/mcp/mcp_bridge.cpp
|
src/mcp/mcp_bridge.cpp
|
||||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||||
@@ -83,14 +93,32 @@ endif()
|
|||||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||||
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
||||||
|
|
||||||
|
# Copy built-in theme JSON files to build directory
|
||||||
|
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||||
|
foreach(_tf ${_theme_files})
|
||||||
|
get_filename_component(_name ${_tf} NAME)
|
||||||
|
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
include(deploy)
|
include(deploy)
|
||||||
|
|
||||||
|
if(TARGET deploy)
|
||||||
add_custom_target(screenshot ALL
|
add_custom_target(screenshot ALL
|
||||||
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||||
DEPENDS Reclass deploy
|
DEPENDS Reclass deploy
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
COMMENT "Capturing UI screenshot with class open..."
|
COMMENT "Capturing UI screenshot with class open..."
|
||||||
)
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
||||||
file(WRITE ${_combine_script} "
|
file(WRITE ${_combine_script} "
|
||||||
@@ -139,13 +167,6 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_compose COMMAND test_compose)
|
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)
|
add_executable(test_provider tests/test_provider.cpp)
|
||||||
target_include_directories(test_provider PRIVATE src)
|
target_include_directories(test_provider PRIVATE src)
|
||||||
@@ -205,6 +226,16 @@ if(BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||||
|
|
||||||
|
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_rendered_view tests/test_rendered_view.cpp
|
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||||
target_include_directories(test_rendered_view PRIVATE src)
|
target_include_directories(test_rendered_view PRIVATE src)
|
||||||
@@ -247,6 +278,47 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||||
add_test(NAME test_theme COMMAND test_theme)
|
add_test(NAME test_theme COMMAND test_theme)
|
||||||
|
|
||||||
|
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_import_xml tests/test_import_xml.cpp
|
||||||
|
src/import_reclass_xml.cpp src/format.cpp src/compose.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/import_source.cpp src/format.cpp src/compose.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/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.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)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
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
|
||||||
|
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||||
|
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
# 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)
|
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||||
if(TARGET ${QT}::windeployqt)
|
if(TARGET ${QT}::windeployqt)
|
||||||
@@ -261,3 +333,7 @@ if(BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(plugins/ProcessMemory)
|
add_subdirectory(plugins/ProcessMemory)
|
||||||
|
if(WIN32)
|
||||||
|
add_subdirectory(plugins/WinDbgMemory)
|
||||||
|
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ This tool helps you inspect raw bytes and interpret them as types (structs, arra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- 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
|
## Build
|
||||||
|
|
||||||
1. Prerequisites
|
1. Prerequisites
|
||||||
|
|||||||
8
deploy/Reclass.desktop
Normal file
8
deploy/Reclass.desktop
Normal 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
|
||||||
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal 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"
|
||||||
|
)
|
||||||
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal 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;
|
||||||
|
}
|
||||||
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal 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;
|
||||||
|
};
|
||||||
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal 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();
|
||||||
|
}
|
||||||
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal 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();
|
||||||
125
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
125
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
uint64_t absAddr = m_base + addr;
|
||||||
|
return m_fns.ReadRemoteMemory(m_handle,
|
||||||
|
reinterpret_cast<RC_Pointer>(absAddr),
|
||||||
|
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;
|
||||||
|
|
||||||
|
uint64_t absAddr = m_base + addr;
|
||||||
|
return m_fns.WriteRemoteMemory(m_handle,
|
||||||
|
reinterpret_cast<RC_Pointer>(absAddr),
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 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;
|
||||||
|
}
|
||||||
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal 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; }
|
||||||
|
void setBase(uint64_t b) override { m_base = b; }
|
||||||
|
QString getSymbol(uint64_t addr) 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;
|
||||||
|
};
|
||||||
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal 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;
|
||||||
|
};
|
||||||
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal 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 = "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||||
|
/// 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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal 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>
|
||||||
34
plugins/WinDbgMemory/CMakeLists.txt
Normal file
34
plugins/WinDbgMemory/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(WinDbgMemoryPlugin 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
|
||||||
|
WinDbgMemoryPlugin.h
|
||||||
|
WinDbgMemoryPlugin.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create shared library (DLL)
|
||||||
|
add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||||
|
|
||||||
|
# Link Qt + DbgEng
|
||||||
|
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets dbgeng ole32)
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
target_include_directories(WinDbgMemoryPlugin PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||||
|
)
|
||||||
|
|
||||||
|
# Output to Plugins folder
|
||||||
|
set_target_properties(WinDbgMemoryPlugin PROPERTIES
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||||
|
)
|
||||||
510
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
Normal file
510
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
#include "WinDbgMemoryPlugin.h"
|
||||||
|
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <dbgeng.h>
|
||||||
|
#pragma comment(lib, "dbgeng.lib")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Thread dispatch helper
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
template<typename Fn>
|
||||||
|
void WinDbgMemoryProvider::dispatchToOwner(Fn&& fn) const
|
||||||
|
{
|
||||||
|
if (!m_dispatcher) { fn(); return; }
|
||||||
|
|
||||||
|
if (QThread::currentThread() == m_dispatcher->thread()) {
|
||||||
|
// Already on the owning thread — call directly
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
// Marshal to the owning thread and block until done
|
||||||
|
QMetaObject::invokeMethod(m_dispatcher, std::forward<Fn>(fn),
|
||||||
|
Qt::BlockingQueuedConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// WinDbgMemoryProvider implementation
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||||
|
{
|
||||||
|
// Create a dedicated thread for all DbgEng COM operations.
|
||||||
|
// DbgEng's remote transport (TCP/named-pipe) is thread-affine — all
|
||||||
|
// calls must happen on the thread that called DebugConnect/DebugCreate.
|
||||||
|
// A private thread with its own event loop guarantees:
|
||||||
|
// 1. dispatchToOwner() works from any calling thread (main, thread-pool, etc.)
|
||||||
|
// 2. No deadlock — the DbgEng thread is never blocked by the caller
|
||||||
|
m_dbgThread = new QThread();
|
||||||
|
m_dbgThread->setObjectName(QStringLiteral("DbgEngThread"));
|
||||||
|
m_dbgThread->start();
|
||||||
|
|
||||||
|
m_dispatcher = new DbgEngDispatcher();
|
||||||
|
m_dispatcher->moveToThread(m_dbgThread);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Run all DbgEng initialization on the dedicated thread.
|
||||||
|
// BlockingQueuedConnection blocks us until the lambda finishes,
|
||||||
|
// so member variables written inside are visible after the call.
|
||||||
|
dispatchToOwner([this, &target]() {
|
||||||
|
HRESULT hr;
|
||||||
|
|
||||||
|
qDebug() << "[WinDbg] Opening target:" << target
|
||||||
|
<< "on DbgEng thread" << QThread::currentThread();
|
||||||
|
|
||||||
|
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||||
|
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
// ── Remote: connect to existing WinDbg debug server ──
|
||||||
|
QByteArray connUtf8 = target.toUtf8();
|
||||||
|
qDebug() << "[WinDbg] DebugConnect:" << target;
|
||||||
|
hr = DebugConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||||
|
qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "client=" << (void*)m_client;
|
||||||
|
if (FAILED(hr) || !m_client) {
|
||||||
|
qWarning() << "[WinDbg] DebugConnect FAILED hr=0x" << Qt::hex << (unsigned long)hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_isRemote = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ── Local: create debug client for pid/dump ──
|
||||||
|
hr = DebugCreate(IID_IDebugClient, (void**)&m_client);
|
||||||
|
qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "client=" << (void*)m_client;
|
||||||
|
if (FAILED(hr) || !m_client) {
|
||||||
|
qWarning() << "[WinDbg] DebugCreate FAILED hr=0x" << Qt::hex << (unsigned long)hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
ULONG pid = target.mid(4).trimmed().toULong(&ok);
|
||||||
|
if (!ok || pid == 0) {
|
||||||
|
qWarning() << "[WinDbg] Invalid PID in target:" << target;
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[WinDbg] Attaching to PID" << pid << "(non-invasive)";
|
||||||
|
hr = m_client->AttachProcess(
|
||||||
|
0, pid,
|
||||||
|
DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND);
|
||||||
|
qDebug() << "[WinDbg] AttachProcess hr=" << Qt::hex << (unsigned long)hr;
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
qWarning() << "[WinDbg] AttachProcess FAILED";
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (target.startsWith("dump:", Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
QString path = target.mid(5).trimmed();
|
||||||
|
QByteArray pathUtf8 = path.toUtf8();
|
||||||
|
|
||||||
|
qDebug() << "[WinDbg] Opening dump file:" << path;
|
||||||
|
hr = m_client->OpenDumpFile(pathUtf8.constData());
|
||||||
|
qDebug() << "[WinDbg] OpenDumpFile hr=" << Qt::hex << (unsigned long)hr;
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
qWarning() << "[WinDbg] OpenDumpFile FAILED";
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qWarning() << "[WinDbg] Unknown target format:" << target;
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initInterfaces();
|
||||||
|
|
||||||
|
// WaitForEvent to finalize the attach/dump load.
|
||||||
|
// For remote connections the server session is already active — skip.
|
||||||
|
if (m_control && !m_isRemote) {
|
||||||
|
qDebug() << "[WinDbg] WaitForEvent...";
|
||||||
|
hr = m_control->WaitForEvent(0, 10000);
|
||||||
|
qDebug() << "[WinDbg] WaitForEvent hr=" << Qt::hex << (unsigned long)hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
querySessionInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
#else
|
||||||
|
Q_UNUSED(target);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinDbgMemoryProvider::initInterfaces()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!m_client) return;
|
||||||
|
|
||||||
|
HRESULT hr;
|
||||||
|
hr = m_client->QueryInterface(IID_IDebugDataSpaces, (void**)&m_dataSpaces);
|
||||||
|
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "ptr=" << (void*)m_dataSpaces;
|
||||||
|
|
||||||
|
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
|
||||||
|
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "ptr=" << (void*)m_control;
|
||||||
|
|
||||||
|
hr = m_client->QueryInterface(IID_IDebugSymbols, (void**)&m_symbols);
|
||||||
|
qDebug() << "[WinDbg] IDebugSymbols hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "ptr=" << (void*)m_symbols;
|
||||||
|
|
||||||
|
if (!m_dataSpaces) {
|
||||||
|
qWarning() << "[WinDbg] No IDebugDataSpaces — cleaning up";
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinDbgMemoryProvider::querySessionInfo()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!m_client) return;
|
||||||
|
HRESULT hr;
|
||||||
|
|
||||||
|
if (m_control) {
|
||||||
|
ULONG debugClass = 0, debugQualifier = 0;
|
||||||
|
hr = m_control->GetDebuggeeType(&debugClass, &debugQualifier);
|
||||||
|
qDebug() << "[WinDbg] GetDebuggeeType hr=" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "class=" << debugClass << "qualifier=" << debugQualifier;
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
m_isLive = (debugQualifier < DEBUG_DUMP_SMALL);
|
||||||
|
m_writable = m_isLive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||||
|
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Dispatch COM cleanup to the DbgEng thread (thread-affine release)
|
||||||
|
if (m_dbgThread && m_dbgThread->isRunning() && m_dispatcher) {
|
||||||
|
dispatchToOwner([this]() {
|
||||||
|
if (m_client) {
|
||||||
|
if (m_isRemote)
|
||||||
|
m_client->EndSession(DEBUG_END_DISCONNECT);
|
||||||
|
else
|
||||||
|
m_client->DetachProcesses();
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Thread not running — clean up directly (best-effort)
|
||||||
|
if (m_client) {
|
||||||
|
if (m_isRemote)
|
||||||
|
m_client->EndSession(DEBUG_END_DISCONNECT);
|
||||||
|
else
|
||||||
|
m_client->DetachProcesses();
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
cleanup();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Stop the dedicated thread
|
||||||
|
if (m_dbgThread) {
|
||||||
|
m_dbgThread->quit();
|
||||||
|
m_dbgThread->wait(3000);
|
||||||
|
delete m_dbgThread;
|
||||||
|
m_dbgThread = nullptr;
|
||||||
|
}
|
||||||
|
delete m_dispatcher;
|
||||||
|
m_dispatcher = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!m_dataSpaces || len <= 0) return false;
|
||||||
|
|
||||||
|
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);
|
||||||
|
result = bytesRead > 0;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!m_dataSpaces || !m_writable || len <= 0) return false;
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
dispatchToOwner([&]() {
|
||||||
|
ULONG bytesWritten = 0;
|
||||||
|
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
|
||||||
|
(ULONG)len, &bytesWritten);
|
||||||
|
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int WinDbgMemoryProvider::size() const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
return m_dataSpaces ? 0x10000 : 0;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WinDbgMemoryProvider::isReadable(uint64_t /*addr*/, int len) const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
// DbgEng's ReadVirtual can read any mapped virtual address.
|
||||||
|
return m_dataSpaces != nullptr && len >= 0;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!m_symbols) return {};
|
||||||
|
|
||||||
|
QString result;
|
||||||
|
dispatchToOwner([&]() {
|
||||||
|
char nameBuf[512] = {};
|
||||||
|
ULONG nameSize = 0;
|
||||||
|
ULONG64 displacement = 0;
|
||||||
|
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
|
||||||
|
&nameSize, &displacement);
|
||||||
|
if (SUCCEEDED(hr) && nameSize > 0) {
|
||||||
|
result = QString::fromUtf8(nameBuf);
|
||||||
|
if (displacement > 0)
|
||||||
|
result += QStringLiteral("+0x%1").arg(displacement, 0, 16);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(addr);
|
||||||
|
return {};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// WinDbgMemoryPlugin implementation
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
QIcon WinDbgMemoryPlugin::Icon() const
|
||||||
|
{
|
||||||
|
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WinDbgMemoryPlugin::canHandle(const QString& target) const
|
||||||
|
{
|
||||||
|
return target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||||
|
|| target.startsWith("npipe:", Qt::CaseInsensitive)
|
||||||
|
|| target.startsWith("pid:", Qt::CaseInsensitive)
|
||||||
|
|| target.startsWith("dump:", Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||||
|
{
|
||||||
|
auto provider = std::make_unique<WinDbgMemoryProvider>(target);
|
||||||
|
if (!provider->isValid())
|
||||||
|
{
|
||||||
|
if (errorMsg) {
|
||||||
|
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||||
|
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||||
|
*errorMsg = QString("Failed to connect to debug server.\n\n"
|
||||||
|
"Target: %1\n\n"
|
||||||
|
"Make sure WinDbg is running with a matching .server command\n"
|
||||||
|
"(e.g. .server tcp:port=5055) and the port/pipe is reachable.")
|
||||||
|
.arg(target);
|
||||||
|
else if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||||
|
*errorMsg = QString("Failed to attach to process.\n\n"
|
||||||
|
"Target: %1\n\n"
|
||||||
|
"Make sure the process is running and you have "
|
||||||
|
"sufficient privileges (try Run as Administrator).")
|
||||||
|
.arg(target);
|
||||||
|
else
|
||||||
|
*errorMsg = QString("Failed to open dump file.\n\n"
|
||||||
|
"Target: %1\n\n"
|
||||||
|
"Make sure the file exists and is a valid dump.")
|
||||||
|
.arg(target);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t WinDbgMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(target);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||||
|
{
|
||||||
|
QDialog dlg(parent);
|
||||||
|
dlg.setWindowTitle("WinDbg Settings");
|
||||||
|
dlg.resize(460, 260);
|
||||||
|
|
||||||
|
QPalette dlgPal = qApp->palette();
|
||||||
|
dlg.setPalette(dlgPal);
|
||||||
|
dlg.setAutoFillBackground(true);
|
||||||
|
|
||||||
|
auto* layout = new QVBoxLayout(&dlg);
|
||||||
|
|
||||||
|
layout->addWidget(new QLabel(
|
||||||
|
"Connect to a running WinDbg debug server.\n"
|
||||||
|
"In WinDbg, run: .server tcp:port=5055"));
|
||||||
|
|
||||||
|
layout->addSpacing(8);
|
||||||
|
layout->addWidget(new QLabel("Connection string:"));
|
||||||
|
auto* connEdit = new QLineEdit;
|
||||||
|
connEdit->setPlaceholderText("tcp:Port=5055,Server=localhost");
|
||||||
|
connEdit->setText("tcp:Port=5055,Server=localhost");
|
||||||
|
layout->addWidget(connEdit);
|
||||||
|
|
||||||
|
layout->addSpacing(4);
|
||||||
|
layout->addWidget(new QLabel("Run one of these in WinDbg first:"));
|
||||||
|
|
||||||
|
auto addExample = [&](const QString& text) {
|
||||||
|
auto* row = new QHBoxLayout;
|
||||||
|
auto* label = new QLabel(text);
|
||||||
|
QPalette lp = dlgPal;
|
||||||
|
lp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||||
|
label->setPalette(lp);
|
||||||
|
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
row->addWidget(label, 1);
|
||||||
|
auto* copyBtn = new QPushButton("Copy");
|
||||||
|
copyBtn->setFixedWidth(50);
|
||||||
|
copyBtn->setToolTip("Copy to clipboard");
|
||||||
|
QObject::connect(copyBtn, &QPushButton::clicked, [text]() {
|
||||||
|
QGuiApplication::clipboard()->setText(text);
|
||||||
|
});
|
||||||
|
row->addWidget(copyBtn);
|
||||||
|
layout->addLayout(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
addExample(".server tcp:port=5055");
|
||||||
|
addExample(".server npipe:pipe=reclass");
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
auto* btnLayout = new QHBoxLayout;
|
||||||
|
btnLayout->addStretch();
|
||||||
|
auto* okBtn = new QPushButton("OK");
|
||||||
|
auto* cancelBtn = new QPushButton("Cancel");
|
||||||
|
btnLayout->addWidget(okBtn);
|
||||||
|
btnLayout->addWidget(cancelBtn);
|
||||||
|
layout->addLayout(btnLayout);
|
||||||
|
|
||||||
|
QObject::connect(okBtn, &QPushButton::clicked, &dlg, &QDialog::accept);
|
||||||
|
QObject::connect(cancelBtn, &QPushButton::clicked, &dlg, &QDialog::reject);
|
||||||
|
|
||||||
|
if (dlg.exec() != QDialog::Accepted)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QString conn = connEdit->text().trimmed();
|
||||||
|
if (conn.isEmpty()) return false;
|
||||||
|
*target = conn;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Plugin factory
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||||
|
{
|
||||||
|
return new WinDbgMemoryPlugin();
|
||||||
|
}
|
||||||
122
plugins/WinDbgMemory/WinDbgMemoryPlugin.h
Normal file
122
plugins/WinDbgMemory/WinDbgMemoryPlugin.h
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../../src/iplugin.h"
|
||||||
|
#include "../../src/core.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
// Forward declarations for DbgEng COM interfaces
|
||||||
|
struct IDebugClient;
|
||||||
|
struct IDebugDataSpaces;
|
||||||
|
struct IDebugControl;
|
||||||
|
struct IDebugSymbols;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WinDbg memory provider
|
||||||
|
*
|
||||||
|
* Uses DbgEng to read memory from:
|
||||||
|
* - An existing WinDbg debug server via DebugConnect (tcp/npipe)
|
||||||
|
* - A live process by PID via DebugCreate (non-invasive attach)
|
||||||
|
* - A crash dump (.dmp) file via DebugCreate
|
||||||
|
*
|
||||||
|
* Target string format:
|
||||||
|
* "tcp:Port=5055,Server=localhost" - connect to WinDbg debug server (TCP)
|
||||||
|
* "npipe:Pipe=name,Server=localhost" - connect to WinDbg debug server (named pipe)
|
||||||
|
* "pid:1234" - attach to process 1234
|
||||||
|
* "dump:C:/path/to/file.dmp" - open dump file
|
||||||
|
*
|
||||||
|
* Threading: All DbgEng COM calls are dispatched to the thread that created
|
||||||
|
* the connection (DebugConnect/DebugCreate). This is required because the
|
||||||
|
* remote transport (TCP/named-pipe) binds to the creating thread. The
|
||||||
|
* controller's background refresh threads call read() which transparently
|
||||||
|
* marshals to the owning thread via BlockingQueuedConnection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helper QObject that lives on the DbgEng-owning thread.
|
||||||
|
// Used as a target for QMetaObject::invokeMethod to marshal calls.
|
||||||
|
class DbgEngDispatcher : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WinDbgMemoryProvider : public rcx::Provider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// Create a provider from a target string
|
||||||
|
WinDbgMemoryProvider(const QString& target);
|
||||||
|
~WinDbgMemoryProvider() override;
|
||||||
|
|
||||||
|
// Required overrides
|
||||||
|
bool read(uint64_t addr, void* buf, int len) const override;
|
||||||
|
int size() const override;
|
||||||
|
|
||||||
|
// Optional overrides
|
||||||
|
bool isReadable(uint64_t addr, int len) const override;
|
||||||
|
bool write(uint64_t addr, const void* buf, int len) override;
|
||||||
|
bool isWritable() const override { return m_writable; }
|
||||||
|
QString name() const override { return m_name; }
|
||||||
|
QString kind() const override { return QStringLiteral("WinDbg"); }
|
||||||
|
QString getSymbol(uint64_t addr) 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; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||||
|
void querySessionInfo(); // determine live/dump, writable, name, base
|
||||||
|
void cleanup();
|
||||||
|
|
||||||
|
// Marshal a lambda to the DbgEng-owning thread. If already on that
|
||||||
|
// thread, calls directly. Otherwise blocks via QueuedConnection.
|
||||||
|
template<typename Fn>
|
||||||
|
void dispatchToOwner(Fn&& fn) const;
|
||||||
|
|
||||||
|
IDebugClient* m_client = nullptr;
|
||||||
|
IDebugDataSpaces* m_dataSpaces = 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;
|
||||||
|
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||||
|
|
||||||
|
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||||
|
// transport is thread-affine — all calls must happen on the thread
|
||||||
|
// that called DebugConnect. A private thread with its own event loop
|
||||||
|
// ensures dispatchToOwner() works from any calling thread (including
|
||||||
|
// QtConcurrent workers and the main/GUI thread) without deadlock.
|
||||||
|
QThread* m_dbgThread = nullptr;
|
||||||
|
DbgEngDispatcher* m_dispatcher = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin that provides WinDbgMemoryProvider
|
||||||
|
*
|
||||||
|
* Uses DbgEng to read memory via:
|
||||||
|
* - Remote connection to an existing WinDbg debug server (tcp/npipe)
|
||||||
|
* - Local non-invasive attach to a live process (pid)
|
||||||
|
* - Local crash dump file (dump)
|
||||||
|
*/
|
||||||
|
class WinDbgMemoryPlugin : public IProviderPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string Name() const override { return "WinDbg Memory"; }
|
||||||
|
std::string Version() const override { return "2.0.0"; }
|
||||||
|
std::string Author() const override { return "Reclass"; }
|
||||||
|
std::string Description() const override { return "Read memory via DbgEng (live process attach or crash dump)"; }
|
||||||
|
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plugin export
|
||||||
|
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 63 KiB |
104
src/compose.cpp
104
src/compose.cpp
@@ -16,6 +16,7 @@ struct ComposeState {
|
|||||||
QVector<LineMeta> meta;
|
QVector<LineMeta> meta;
|
||||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||||
|
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
|
||||||
int currentLine = 0;
|
int currentLine = 0;
|
||||||
int typeW = kColType; // global type column width (fallback)
|
int typeW = kColType; // global type column width (fallback)
|
||||||
int nameW = kColName; // global name column width (fallback)
|
int nameW = kColName; // global name column width (fallback)
|
||||||
@@ -64,7 +65,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
|
|||||||
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
||||||
uint32_t mask = 0;
|
uint32_t mask = 0;
|
||||||
if (isCont) mask |= (1u << M_CONT);
|
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.
|
// No ambient validation markers — errors only shown during inline editing.
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
@@ -118,14 +118,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
int typeW = state.effectiveTypeW(scopeId);
|
int typeW = state.effectiveTypeW(scopeId);
|
||||||
int nameW = state.effectiveNameW(scopeId);
|
int nameW = state.effectiveNameW(scopeId);
|
||||||
|
|
||||||
// Line count: padding wraps at 8 bytes per line
|
int numLines = linesForKind(node.kind);
|
||||||
int numLines;
|
|
||||||
if (node.kind == NodeKind::Padding) {
|
|
||||||
int totalBytes = qMax(1, node.arrayLen);
|
|
||||||
numLines = (totalBytes + 7) / 8;
|
|
||||||
} else {
|
|
||||||
numLines = linesForKind(node.kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve pointer target name for display
|
// Resolve pointer target name for display
|
||||||
QString ptrTypeOverride;
|
QString ptrTypeOverride;
|
||||||
@@ -156,13 +149,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
|
|
||||||
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
||||||
if (isHexPreview(node.kind)) {
|
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,
|
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
||||||
@@ -345,12 +333,36 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
});
|
});
|
||||||
|
// Use the referenced struct's scope widths (children come from there)
|
||||||
|
uint64_t refScopeId = node.refId;
|
||||||
for (int childIdx : refChildren) {
|
for (int childIdx : refChildren) {
|
||||||
// Skip self-referential children (e.g. struct Ball has a field of type Ball)
|
const Node& child = tree.nodes[childIdx];
|
||||||
if (state.visiting.contains(tree.nodes[childIdx].id))
|
// Self-referential child → show as collapsed struct (non-expandable)
|
||||||
|
if (state.visiting.contains(child.id)) {
|
||||||
|
int typeW = state.effectiveTypeW(refScopeId);
|
||||||
|
int nameW = state.effectiveNameW(refScopeId);
|
||||||
|
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,
|
||||||
|
state.offsetHexDigits);
|
||||||
|
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
||||||
|
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.effectiveNameW = nameW;
|
||||||
|
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||||
|
/*collapsed=*/true, typeW, nameW), lm);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
composeNode(state, tree, prov, childIdx, childDepth,
|
composeNode(state, tree, prov, childIdx, childDepth,
|
||||||
absAddr, node.refId, false, node.id);
|
absAddr, node.refId, false, refScopeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,39 +418,57 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||||
|
|
||||||
|
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||||
|
QVector<int> ptrChildren = state.childMap.value(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)
|
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
|
||||||
{
|
{
|
||||||
LineMeta lm;
|
LineMeta lm;
|
||||||
lm.nodeIdx = nodeIdx;
|
lm.nodeIdx = nodeIdx;
|
||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = effectiveCollapsed;
|
||||||
lm.foldLevel = computeFoldLevel(depth, true);
|
lm.foldLevel = computeFoldLevel(depth, true);
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||||
|
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||||
lm.effectiveTypeW = typeW;
|
lm.effectiveTypeW = typeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
lm.pointerTargetName = ptrTargetName;
|
lm.pointerTargetName = ptrTargetName;
|
||||||
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
|
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||||
prov, absAddr, ptrTypeOverride,
|
prov, absAddr, ptrTypeOverride,
|
||||||
typeW, nameW), lm);
|
typeW, nameW), lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.collapsed) {
|
if (!effectiveCollapsed) {
|
||||||
int sz = node.byteSize();
|
int sz = node.byteSize();
|
||||||
uint64_t ptrVal = 0;
|
uint64_t ptrVal = 0;
|
||||||
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
||||||
ptrVal = (node.kind == NodeKind::Pointer32)
|
ptrVal = (node.kind == NodeKind::Pointer32)
|
||||||
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
|
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
|
||||||
if (ptrVal != 0) {
|
if (ptrVal != 0) {
|
||||||
|
// 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);
|
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
||||||
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if pointer target is actually readable
|
// Determine if pointer target is actually readable
|
||||||
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
|
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
|
||||||
@@ -451,19 +481,43 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
if (!ptrReadable)
|
if (!ptrReadable)
|
||||||
pBase = (uint64_t)0 - tree.baseAddress;
|
pBase = (uint64_t)0 - tree.baseAddress;
|
||||||
|
|
||||||
|
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.
|
||||||
|
std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
|
||||||
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
|
});
|
||||||
|
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);
|
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||||
if (!state.ptrVisiting.contains(key)) {
|
if (!state.ptrVisiting.contains(key)) {
|
||||||
state.ptrVisiting.insert(key);
|
state.ptrVisiting.insert(key);
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
int refIdx = tree.indexOfId(node.refId);
|
||||||
if (refIdx >= 0) {
|
if (refIdx >= 0) {
|
||||||
const Node& ref = tree.nodes[refIdx];
|
const Node& ref = tree.nodes[refIdx];
|
||||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
|
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,
|
composeParent(state, tree, childProv, refIdx,
|
||||||
depth, pBase, ref.id,
|
depth, pBase, ref.id,
|
||||||
/*isArrayChild=*/true);
|
/*isArrayChild=*/true);
|
||||||
|
state.virtualPtrRefs.remove(node.refId);
|
||||||
|
if (wasVisiting) state.visiting.insert(node.refId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.ptrVisiting.remove(key);
|
state.ptrVisiting.remove(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Footer for pointer fold
|
// Footer for pointer fold
|
||||||
{
|
{
|
||||||
@@ -542,7 +596,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
// Include struct/array names - they now use columnar layout too
|
// Include struct/array names - they now use columnar layout too
|
||||||
int maxNameLen = kMinNameW;
|
int maxNameLen = kMinNameW;
|
||||||
for (const Node& node : tree.nodes) {
|
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;
|
if (isHexPreview(node.kind)) continue;
|
||||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||||
}
|
}
|
||||||
@@ -561,7 +615,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
const Node& child = tree.nodes[childIdx];
|
const Node& child = tree.nodes[childIdx];
|
||||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
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)) {
|
if (!isHexPreview(child.kind)) {
|
||||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||||
}
|
}
|
||||||
@@ -593,7 +647,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
const Node& child = tree.nodes[childIdx];
|
const Node& child = tree.nodes[childIdx];
|
||||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||||
|
|
||||||
// Name width (skip hex/padding, include containers)
|
// Name width (skip hex, include containers)
|
||||||
if (!isHexPreview(child.kind)) {
|
if (!isHexPreview(child.kind)) {
|
||||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,14 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
|||||||
editor->applyDocument(m_lastResult);
|
editor->applyDocument(m_lastResult);
|
||||||
}
|
}
|
||||||
updateCommandRow();
|
updateCommandRow();
|
||||||
|
|
||||||
|
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
|
||||||
|
if (!m_cachedPopup) {
|
||||||
|
QTimer::singleShot(0, this, [this, editor]() {
|
||||||
|
if (!m_cachedPopup && !m_editors.isEmpty())
|
||||||
|
ensurePopup(editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,8 +234,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
switch (target) {
|
switch (target) {
|
||||||
case EditTarget::Name: {
|
case EditTarget::Name: {
|
||||||
if (text.isEmpty()) break;
|
if (text.isEmpty()) break;
|
||||||
|
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
// ASCII edit on Hex/Padding nodes
|
// ASCII edit on Hex nodes
|
||||||
if (isHexPreview(node.kind)) {
|
if (isHexPreview(node.kind)) {
|
||||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
||||||
} else {
|
} else {
|
||||||
@@ -293,6 +302,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
break;
|
break;
|
||||||
case EditTarget::BaseAddress: {
|
case EditTarget::BaseAddress: {
|
||||||
QString s = text.trimmed();
|
QString s = text.trimmed();
|
||||||
|
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
||||||
|
s.remove('\n');
|
||||||
|
s.remove('\r');
|
||||||
// Support simple equations: 0x10+0x4, 0x100-0x10, etc.
|
// Support simple equations: 0x10+0x4, 0x100-0x10, etc.
|
||||||
uint64_t newBase = 0;
|
uint64_t newBase = 0;
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
@@ -347,7 +359,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
if (text.startsWith(QStringLiteral("#saved:"))) {
|
if (text.startsWith(QStringLiteral("#saved:"))) {
|
||||||
int idx = text.mid(7).toInt();
|
int idx = text.mid(7).toInt();
|
||||||
switchToSavedSource(idx);
|
switchToSavedSource(idx);
|
||||||
} else if (text == QStringLiteral("file")) {
|
} else if (text == QStringLiteral("File")) {
|
||||||
auto* w = qobject_cast<QWidget*>(parent());
|
auto* w = qobject_cast<QWidget*>(parent());
|
||||||
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
|
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
|
||||||
if (!path.isEmpty()) {
|
if (!path.isEmpty()) {
|
||||||
@@ -424,7 +436,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
m_doc->undoStack.clear();
|
m_doc->undoStack.clear();
|
||||||
m_doc->provider = std::move(provider);
|
m_doc->provider = std::move(provider);
|
||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
|
else
|
||||||
|
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
|
|
||||||
@@ -599,7 +614,7 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
if (isHexPreview(node.kind)) {
|
if (isHexPreview(node.kind)) {
|
||||||
// Per-byte tracking for hex preview nodes
|
// Per-byte tracking for hex preview nodes
|
||||||
int lineOff = (node.kind == NodeKind::Padding) ? lm.subLine * 8 : 0;
|
int lineOff = 0;
|
||||||
int byteCount = lm.lineByteCount;
|
int byteCount = lm.lineByteCount;
|
||||||
for (int b = 0; b < byteCount; b++) {
|
for (int b = 0; b < byteCount; b++) {
|
||||||
if (m_changedOffsets.contains(offset + lineOff + b)) {
|
if (m_changedOffsets.contains(offset + lineOff + b)) {
|
||||||
@@ -621,6 +636,39 @@ void RcxController::refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update value history and compute heat levels
|
||||||
|
// Only run when a live provider is attached (not for static file/buffer sources)
|
||||||
|
{
|
||||||
|
const Provider* prov = nullptr;
|
||||||
|
if (m_snapshotProv && m_snapshotProv->isLive())
|
||||||
|
prov = m_snapshotProv.get();
|
||||||
|
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
|
||||||
|
prov = m_doc->provider.get();
|
||||||
|
|
||||||
|
if (prov) {
|
||||||
|
for (auto& lm : m_lastResult.meta) {
|
||||||
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||||
|
if (isSyntheticLine(lm) || lm.isContinuation) continue;
|
||||||
|
if (lm.lineKind != LineKind::Field) continue;
|
||||||
|
|
||||||
|
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
||||||
|
// Skip containers — they don't have scalar values
|
||||||
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
|
||||||
|
|
||||||
|
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||||
|
uint64_t addr = static_cast<uint64_t>(nodeOff); // provider-relative
|
||||||
|
int sz = node.byteSize();
|
||||||
|
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
||||||
|
|
||||||
|
QString val = fmt::readValue(node, *prov, addr, lm.subLine);
|
||||||
|
if (!val.isEmpty()) {
|
||||||
|
m_valueHistory[lm.nodeId].record(val);
|
||||||
|
lm.heatLevel = m_valueHistory[lm.nodeId].heatLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||||
QSet<uint64_t> valid;
|
QSet<uint64_t> valid;
|
||||||
for (uint64_t id : m_selIds) {
|
for (uint64_t id : m_selIds) {
|
||||||
@@ -644,13 +692,16 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
for (auto* editor : m_editors) {
|
for (auto* editor : m_editors) {
|
||||||
editor->setCustomTypeNames(customTypes);
|
editor->setCustomTypeNames(customTypes);
|
||||||
|
editor->setValueHistoryRef(&m_valueHistory);
|
||||||
ViewState vs = editor->saveViewState();
|
ViewState vs = editor->saveViewState();
|
||||||
editor->applyDocument(m_lastResult);
|
editor->applyDocument(m_lastResult);
|
||||||
editor->restoreViewState(vs);
|
editor->restoreViewState(vs);
|
||||||
}
|
}
|
||||||
applySelectionOverlays();
|
// Text-modifying passes first (command row replaces line 0 text),
|
||||||
|
// then overlays last so hover indicators survive the refresh.
|
||||||
pushSavedSourcesToEditors();
|
pushSavedSourcesToEditors();
|
||||||
updateCommandRow();
|
updateCommandRow();
|
||||||
|
applySelectionOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
||||||
@@ -792,9 +843,78 @@ void RcxController::toggleCollapse(int nodeIdx) {
|
|||||||
cmd::Collapse{node.id, node.collapsed, !node.collapsed}));
|
cmd::Collapse{node.id, node.collapsed, !node.collapsed}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::materializeRefChildren(int nodeIdx) {
|
||||||
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
|
auto& tree = m_doc->tree;
|
||||||
|
|
||||||
|
// Snapshot values before any mutation invalidates references
|
||||||
|
const uint64_t parentId = tree.nodes[nodeIdx].id;
|
||||||
|
const uint64_t refId = tree.nodes[nodeIdx].refId;
|
||||||
|
const NodeKind parentKind = tree.nodes[nodeIdx].kind;
|
||||||
|
const QString parentName = tree.nodes[nodeIdx].name;
|
||||||
|
|
||||||
|
if (refId == 0) return;
|
||||||
|
if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized
|
||||||
|
|
||||||
|
// Collect children to clone (copy by value to avoid reference invalidation)
|
||||||
|
QVector<int> refChildren = tree.childrenOf(refId);
|
||||||
|
if (refChildren.isEmpty()) return;
|
||||||
|
|
||||||
|
QVector<Node> clones;
|
||||||
|
clones.reserve(refChildren.size());
|
||||||
|
for (int ci : refChildren) {
|
||||||
|
Node copy = tree.nodes[ci]; // copy by value before any mutation
|
||||||
|
copy.id = tree.reserveId();
|
||||||
|
copy.parentId = parentId;
|
||||||
|
copy.collapsed = true;
|
||||||
|
clones.append(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap all mutations in an undo macro
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Materialize ref children"));
|
||||||
|
|
||||||
|
for (const Node& clone : clones) {
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Insert{clone, {}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-expand the self-referential child (the one that was the cycle)
|
||||||
|
// so the user gets expand in a single click
|
||||||
|
for (const Node& clone : clones) {
|
||||||
|
if (clone.kind == parentKind && clone.name == parentName && clone.refId == refId) {
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Collapse{clone.id, true, false}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
if (!m_suppressRefresh) refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||||
auto& tree = m_doc->tree;
|
auto& tree = m_doc->tree;
|
||||||
|
|
||||||
|
// Clear value history for nodes whose effective offset changed.
|
||||||
|
// When offsets shift (insert/delete/resize), old recorded values came from
|
||||||
|
// a different memory address, so keeping them would show false heat.
|
||||||
|
// Also invalidates any in-flight async read so that stale snapshot data
|
||||||
|
// from before the offset change doesn't re-introduce false heat.
|
||||||
|
auto clearHistoryForAdjs = [&](const QVector<cmd::OffsetAdj>& adjs) {
|
||||||
|
if (adjs.isEmpty()) return;
|
||||||
|
m_refreshGen++; // discard in-flight async read (stale layout)
|
||||||
|
for (const auto& adj : adjs) {
|
||||||
|
// Clear the adjusted node itself
|
||||||
|
m_valueHistory.remove(adj.nodeId);
|
||||||
|
// Clear all descendants (their effective address also shifted)
|
||||||
|
for (int ci : tree.subtreeIndices(adj.nodeId))
|
||||||
|
m_valueHistory.remove(tree.nodes[ci].id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
std::visit([&](auto&& c) {
|
std::visit([&](auto&& c) {
|
||||||
using T = std::decay_t<decltype(c)>;
|
using T = std::decay_t<decltype(c)>;
|
||||||
if constexpr (std::is_same_v<T, cmd::ChangeKind>) {
|
if constexpr (std::is_same_v<T, cmd::ChangeKind>) {
|
||||||
@@ -806,6 +926,12 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
if (ai >= 0)
|
if (ai >= 0)
|
||||||
tree.nodes[ai].offset = isUndo ? adj.oldOffset : adj.newOffset;
|
tree.nodes[ai].offset = isUndo ? adj.oldOffset : adj.newOffset;
|
||||||
}
|
}
|
||||||
|
// The changed node's value format changed; clear its history.
|
||||||
|
// If offAdjs is empty (same-size change), still bump gen to
|
||||||
|
// discard in-flight reads that would record the old format.
|
||||||
|
if (c.offAdjs.isEmpty()) m_refreshGen++;
|
||||||
|
m_valueHistory.remove(c.nodeId);
|
||||||
|
clearHistoryForAdjs(c.offAdjs);
|
||||||
} else if constexpr (std::is_same_v<T, cmd::Rename>) {
|
} else if constexpr (std::is_same_v<T, cmd::Rename>) {
|
||||||
int idx = tree.indexOfId(c.nodeId);
|
int idx = tree.indexOfId(c.nodeId);
|
||||||
if (idx >= 0)
|
if (idx >= 0)
|
||||||
@@ -834,6 +960,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearHistoryForAdjs(c.offAdjs);
|
||||||
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
||||||
if (isUndo) {
|
if (isUndo) {
|
||||||
// Restore nodes first
|
// Restore nodes first
|
||||||
@@ -850,13 +977,17 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
int ai = tree.indexOfId(adj.nodeId);
|
int ai = tree.indexOfId(adj.nodeId);
|
||||||
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||||
}
|
}
|
||||||
// Remove nodes
|
// Remove nodes and their value history
|
||||||
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
||||||
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
||||||
for (int idx : indices)
|
for (int idx : indices) {
|
||||||
|
m_valueHistory.remove(tree.nodes[idx].id);
|
||||||
tree.nodes.remove(idx);
|
tree.nodes.remove(idx);
|
||||||
|
}
|
||||||
tree.invalidateIdCache();
|
tree.invalidateIdCache();
|
||||||
}
|
}
|
||||||
|
// Siblings shifted — their old values are from wrong addresses
|
||||||
|
clearHistoryForAdjs(c.offAdjs);
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
||||||
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
||||||
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
|
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
|
||||||
@@ -868,11 +999,14 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
||||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||||
if (!m_doc->provider->writeBytes(c.addr, bytes))
|
// Write through snapshot (patches pages only on success) or provider directly.
|
||||||
|
// If write fails, the snapshot is NOT patched, so the next compose shows the
|
||||||
|
// real unchanged value — no optimistic visual leak.
|
||||||
|
bool ok = m_snapshotProv
|
||||||
|
? m_snapshotProv->write(c.addr, bytes.constData(), bytes.size())
|
||||||
|
: m_doc->provider->writeBytes(c.addr, bytes);
|
||||||
|
if (!ok)
|
||||||
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
|
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
|
||||||
// Patch snapshot so compose sees the new value immediately
|
|
||||||
if (m_snapshotProv)
|
|
||||||
m_snapshotProv->patchSnapshot(c.addr, bytes.constData(), bytes.size());
|
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
||||||
int idx = tree.indexOfId(c.nodeId);
|
int idx = tree.indexOfId(c.nodeId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -900,6 +1034,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
int idx = tree.indexOfId(c.nodeId);
|
int idx = tree.indexOfId(c.nodeId);
|
||||||
if (idx >= 0)
|
if (idx >= 0)
|
||||||
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
|
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
|
||||||
|
// Node and its descendants read from a different address now
|
||||||
|
m_refreshGen++; // discard in-flight async read (stale layout)
|
||||||
|
m_valueHistory.remove(c.nodeId);
|
||||||
|
for (int ci : tree.subtreeIndices(c.nodeId))
|
||||||
|
m_valueHistory.remove(tree.nodes[ci].id);
|
||||||
}
|
}
|
||||||
}, command);
|
}, command);
|
||||||
|
|
||||||
@@ -955,8 +1094,21 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
|
|||||||
// Validate write range before pushing command
|
// Validate write range before pushing command
|
||||||
if (!m_doc->provider->isReadable(addr, writeSize)) return;
|
if (!m_doc->provider->isReadable(addr, writeSize)) return;
|
||||||
|
|
||||||
|
// Read old bytes before writing (for undo)
|
||||||
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
|
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
|
||||||
|
|
||||||
|
// Test the write first — don't push a command that will silently fail.
|
||||||
|
// This prevents optimistic visual updates for read-only providers.
|
||||||
|
bool writeOk = m_snapshotProv
|
||||||
|
? m_snapshotProv->write(addr, newBytes.constData(), newBytes.size())
|
||||||
|
: m_doc->provider->writeBytes(addr, newBytes);
|
||||||
|
if (!writeOk) {
|
||||||
|
qWarning() << "Write failed at address" << QString::number(addr, 16);
|
||||||
|
refresh(); // refresh to show the real unchanged value
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write succeeded — push undo command (redo will write again, which is harmless)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
||||||
}
|
}
|
||||||
@@ -1052,23 +1204,23 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
// Quick-convert suggestions for Hex nodes
|
// Quick-convert suggestions for Hex nodes
|
||||||
bool addedQuickConvert = false;
|
bool addedQuickConvert = false;
|
||||||
if (node.kind == NodeKind::Hex64) {
|
if (node.kind == NodeKind::Hex64) {
|
||||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint64_t", [this, nodeId]() {
|
menu.addAction("Change to uint64_t", [this, nodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64);
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64);
|
||||||
});
|
});
|
||||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() {
|
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||||
});
|
});
|
||||||
addedQuickConvert = true;
|
addedQuickConvert = true;
|
||||||
} else if (node.kind == NodeKind::Hex32) {
|
} else if (node.kind == NodeKind::Hex32) {
|
||||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() {
|
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||||
});
|
});
|
||||||
addedQuickConvert = true;
|
addedQuickConvert = true;
|
||||||
} else if (node.kind == NodeKind::Hex16) {
|
} else if (node.kind == NodeKind::Hex16) {
|
||||||
menu.addAction(icon("symbol-numeric.svg"), "Change to int16_t", [this, nodeId]() {
|
menu.addAction("Change to int16_t", [this, nodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
if (ni >= 0) changeNodeKind(ni, NodeKind::Int16);
|
if (ni >= 0) changeNodeKind(ni, NodeKind::Int16);
|
||||||
});
|
});
|
||||||
@@ -1078,7 +1230,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
||||||
&& node.kind != NodeKind::Padding
|
|
||||||
&& m_doc->provider->isWritable();
|
&& m_doc->provider->isWritable();
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
|
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
|
||||||
@@ -1094,6 +1245,51 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
editor->beginInlineEdit(EditTarget::Type, line);
|
editor->beginInlineEdit(EditTarget::Type, line);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
|
||||||
|
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
|
||||||
|
menu.addAction("Convert to &Hex", [this, nodeId]() {
|
||||||
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
|
if (ni < 0) return;
|
||||||
|
const Node& n = m_doc->tree.nodes[ni];
|
||||||
|
int totalSize = n.byteSize();
|
||||||
|
if (totalSize <= 0) return;
|
||||||
|
|
||||||
|
uint64_t parentId = n.parentId;
|
||||||
|
int baseOffset = n.offset;
|
||||||
|
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
|
||||||
|
|
||||||
|
// Remove the original node
|
||||||
|
QVector<Node> subtree;
|
||||||
|
subtree.append(n);
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Remove{nodeId, subtree, {}}));
|
||||||
|
|
||||||
|
// Insert hex nodes to fill the space (largest first)
|
||||||
|
int padOffset = baseOffset;
|
||||||
|
int gap = totalSize;
|
||||||
|
while (gap > 0) {
|
||||||
|
NodeKind padKind;
|
||||||
|
int padSize;
|
||||||
|
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
||||||
|
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
||||||
|
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
||||||
|
else { padKind = NodeKind::Hex8; padSize = 1; }
|
||||||
|
|
||||||
|
insertNode(parentId, padOffset, padKind,
|
||||||
|
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
|
||||||
|
padOffset += padSize;
|
||||||
|
gap -= padSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
if (!m_suppressRefresh) refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
||||||
@@ -1321,8 +1517,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applySelectionOverlays();
|
|
||||||
updateCommandRow();
|
updateCommandRow();
|
||||||
|
applySelectionOverlays();
|
||||||
|
|
||||||
if (m_selIds.size() == 1) {
|
if (m_selIds.size() == 1) {
|
||||||
uint64_t sid = *m_selIds.begin();
|
uint64_t sid = *m_selIds.begin();
|
||||||
@@ -1335,8 +1531,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
void RcxController::clearSelection() {
|
void RcxController::clearSelection() {
|
||||||
m_selIds.clear();
|
m_selIds.clear();
|
||||||
m_anchorLine = -1;
|
m_anchorLine = -1;
|
||||||
applySelectionOverlays();
|
|
||||||
updateCommandRow();
|
updateCommandRow();
|
||||||
|
applySelectionOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::applySelectionOverlays() {
|
void RcxController::applySelectionOverlays() {
|
||||||
@@ -1355,17 +1551,17 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
|||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Separate into real nodes (non-Padding) and padding nodes
|
// Separate into real nodes (non-hex) and hex filler nodes
|
||||||
struct NodeInfo { uint64_t id; int offset; int size; };
|
struct NodeInfo { uint64_t id; int offset; int size; };
|
||||||
QVector<NodeInfo> realNodes;
|
QVector<NodeInfo> realNodes;
|
||||||
QVector<uint64_t> padIds;
|
QVector<uint64_t> hexIds;
|
||||||
|
|
||||||
for (int ci : kids) {
|
for (int ci : kids) {
|
||||||
const Node& child = tree.nodes[ci];
|
const Node& child = tree.nodes[ci];
|
||||||
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||||
? tree.structSpan(child.id) : child.byteSize();
|
? tree.structSpan(child.id) : child.byteSize();
|
||||||
if (child.kind == NodeKind::Padding)
|
if (isHexNode(child.kind))
|
||||||
padIds.append(child.id);
|
hexIds.append(child.id);
|
||||||
else
|
else
|
||||||
realNodes.append({child.id, child.offset, sz});
|
realNodes.append({child.id, child.offset, sz});
|
||||||
}
|
}
|
||||||
@@ -1398,7 +1594,7 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if anything actually changes
|
// Check if anything actually changes
|
||||||
if (offChanges.isEmpty() && padIds.isEmpty() && padsNeeded.isEmpty())
|
if (offChanges.isEmpty() && hexIds.isEmpty() && padsNeeded.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Apply as undoable macro
|
// Apply as undoable macro
|
||||||
@@ -1406,14 +1602,14 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
|||||||
m_suppressRefresh = true;
|
m_suppressRefresh = true;
|
||||||
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
|
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
|
||||||
|
|
||||||
// 1. Remove all existing Padding nodes (no offset adjustments — we recompute)
|
// 1. Remove all existing hex filler nodes (no offset adjustments — we recompute)
|
||||||
for (uint64_t pid : padIds) {
|
for (uint64_t hid : hexIds) {
|
||||||
int idx = tree.indexOfId(pid);
|
int idx = tree.indexOfId(hid);
|
||||||
if (idx < 0) continue;
|
if (idx < 0) continue;
|
||||||
QVector<Node> subtree;
|
QVector<Node> subtree;
|
||||||
subtree.append(tree.nodes[idx]);
|
subtree.append(tree.nodes[idx]);
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::Remove{pid, subtree, {}}));
|
cmd::Remove{hid, subtree, {}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Reposition real nodes
|
// 2. Reposition real nodes
|
||||||
@@ -1422,15 +1618,28 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
|||||||
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
|
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Insert new padding in gaps
|
// 3. Insert hex nodes to fill gaps (largest first for alignment)
|
||||||
for (const auto& pi : padsNeeded) {
|
for (const auto& pi : padsNeeded) {
|
||||||
|
int padOffset = pi.offset;
|
||||||
|
int gap = pi.size;
|
||||||
|
while (gap > 0) {
|
||||||
|
NodeKind padKind;
|
||||||
|
int padSize;
|
||||||
|
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
||||||
|
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
||||||
|
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
||||||
|
else { padKind = NodeKind::Hex8; padSize = 1; }
|
||||||
|
|
||||||
Node pad;
|
Node pad;
|
||||||
pad.kind = NodeKind::Padding;
|
pad.kind = padKind;
|
||||||
pad.parentId = structId;
|
pad.parentId = structId;
|
||||||
pad.offset = pi.offset;
|
pad.offset = padOffset;
|
||||||
pad.arrayLen = pi.size;
|
pad.name = QString("pad_%1").arg(padOffset, 2, 16, QChar('0'));
|
||||||
pad.id = tree.reserveId();
|
pad.id = tree.reserveId();
|
||||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
|
||||||
|
padOffset += padSize;
|
||||||
|
gap -= padSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_doc->undoStack.endMacro();
|
m_doc->undoStack.endMacro();
|
||||||
@@ -1534,7 +1743,6 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
|
|
||||||
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
||||||
for (const auto& m : kKindMeta) {
|
for (const auto& m : kKindMeta) {
|
||||||
if (m.kind == NodeKind::Padding) continue;
|
|
||||||
if (excludeStructArrayPad &&
|
if (excludeStructArrayPad &&
|
||||||
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
|
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
|
||||||
continue;
|
continue;
|
||||||
@@ -1565,7 +1773,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case TypePopupMode::Root:
|
case TypePopupMode::Root:
|
||||||
addPrimitives(/*enabled=*/false, /*excludeStructArrayPad=*/false);
|
// No primitives in Root mode – only project types are valid roots
|
||||||
addComposites([&](const Node&, const TypeEntry& e) {
|
addComposites([&](const Node&, const TypeEntry& e) {
|
||||||
return e.structId == m_viewRootId;
|
return e.structId == m_viewRootId;
|
||||||
});
|
});
|
||||||
@@ -1669,6 +1877,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
});
|
});
|
||||||
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
||||||
this, [this, mode, nodeIdx]() {
|
this, [this, mode, nodeIdx]() {
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
|
||||||
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Struct;
|
n.kind = NodeKind::Struct;
|
||||||
n.name = QString();
|
n.name = QString();
|
||||||
@@ -1676,6 +1888,16 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
n.offset = 0;
|
n.offset = 0;
|
||||||
n.id = m_doc->tree.reserveId();
|
n.id = m_doc->tree.reserveId();
|
||||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
||||||
|
|
||||||
|
// Populate with default hex nodes (8 x Hex64 = 64 bytes)
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
insertNode(n.id, i * 8, NodeKind::Hex64,
|
||||||
|
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
|
||||||
TypeEntry newEntry;
|
TypeEntry newEntry;
|
||||||
newEntry.entryKind = TypeEntry::Composite;
|
newEntry.entryKind = TypeEntry::Composite;
|
||||||
newEntry.structId = n.id;
|
newEntry.structId = n.id;
|
||||||
@@ -1694,14 +1916,22 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
|
||||||
|
// BUG-1 fix: Copy needed fields to locals before any mutation.
|
||||||
|
// changeNodeKind() can trigger insertNode() → addNode() → nodes.append(),
|
||||||
|
// which may reallocate the QVector, invalidating any reference into it.
|
||||||
|
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
|
||||||
|
const NodeKind nodeKind = m_doc->tree.nodes[nodeIdx].kind;
|
||||||
|
const NodeKind elemKind = m_doc->tree.nodes[nodeIdx].elementKind;
|
||||||
|
const uint64_t nodeRefId = m_doc->tree.nodes[nodeIdx].refId;
|
||||||
|
const int arrLen = m_doc->tree.nodes[nodeIdx].arrayLen;
|
||||||
|
|
||||||
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
|
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
|
||||||
TypeSpec spec = parseTypeSpec(fullText);
|
TypeSpec spec = parseTypeSpec(fullText);
|
||||||
|
|
||||||
if (mode == TypePopupMode::FieldType) {
|
if (mode == TypePopupMode::FieldType) {
|
||||||
if (entry.entryKind == TypeEntry::Primitive) {
|
if (entry.entryKind == TypeEntry::Primitive) {
|
||||||
if (entry.primitiveKind != node.kind)
|
if (entry.primitiveKind != nodeKind)
|
||||||
changeNodeKind(nodeIdx, entry.primitiveKind);
|
changeNodeKind(nodeIdx, entry.primitiveKind);
|
||||||
} else if (entry.entryKind == TypeEntry::Composite) {
|
} else if (entry.entryKind == TypeEntry::Composite) {
|
||||||
bool wasSuppressed = m_suppressRefresh;
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
@@ -1710,34 +1940,34 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|||||||
|
|
||||||
if (spec.isPointer) {
|
if (spec.isPointer) {
|
||||||
// Pointer modifier: e.g. "Material*" → Pointer64 + refId
|
// Pointer modifier: e.g. "Material*" → Pointer64 + refId
|
||||||
if (node.kind != NodeKind::Pointer64)
|
if (nodeKind != NodeKind::Pointer64)
|
||||||
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
||||||
int idx = m_doc->tree.indexOfId(node.id);
|
int idx = m_doc->tree.indexOfId(nodeId);
|
||||||
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId)
|
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId}));
|
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||||
|
|
||||||
} else if (spec.arrayCount > 0) {
|
} else if (spec.arrayCount > 0) {
|
||||||
// Array modifier: e.g. "Material[10]" → Array + Struct element
|
// Array modifier: e.g. "Material[10]" → Array + Struct element
|
||||||
if (node.kind != NodeKind::Array)
|
if (nodeKind != NodeKind::Array)
|
||||||
changeNodeKind(nodeIdx, NodeKind::Array);
|
changeNodeKind(nodeIdx, NodeKind::Array);
|
||||||
int idx = m_doc->tree.indexOfId(node.id);
|
int idx = m_doc->tree.indexOfId(nodeId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
auto& n = m_doc->tree.nodes[idx];
|
auto& n = m_doc->tree.nodes[idx];
|
||||||
if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount)
|
if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeArrayMeta{node.id, n.elementKind, NodeKind::Struct,
|
cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
|
||||||
n.arrayLen, spec.arrayCount}));
|
n.arrayLen, spec.arrayCount}));
|
||||||
if (n.refId != entry.structId)
|
if (n.refId != entry.structId)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, n.refId, entry.structId}));
|
cmd::ChangePointerRef{nodeId, n.refId, entry.structId}));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed
|
// Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed
|
||||||
if (node.kind != NodeKind::Struct)
|
if (nodeKind != NodeKind::Struct)
|
||||||
changeNodeKind(nodeIdx, NodeKind::Struct);
|
changeNodeKind(nodeIdx, NodeKind::Struct);
|
||||||
int idx = m_doc->tree.indexOfId(node.id);
|
int idx = m_doc->tree.indexOfId(nodeId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
int refIdx = m_doc->tree.indexOfId(entry.structId);
|
int refIdx = m_doc->tree.indexOfId(entry.structId);
|
||||||
QString targetName;
|
QString targetName;
|
||||||
@@ -1748,11 +1978,11 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|||||||
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
|
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
|
||||||
if (oldTypeName != targetName)
|
if (oldTypeName != targetName)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeStructTypeName{node.id, oldTypeName, targetName}));
|
cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
|
||||||
// Set refId so compose can expand the referenced struct's children
|
// Set refId so compose can expand the referenced struct's children
|
||||||
if (m_doc->tree.nodes[idx].refId != entry.structId)
|
if (m_doc->tree.nodes[idx].refId != entry.structId)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId}));
|
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||||
// ChangePointerRef auto-sets collapsed=true when refId != 0
|
// ChangePointerRef auto-sets collapsed=true when refId != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1763,33 +1993,32 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|||||||
}
|
}
|
||||||
} else if (mode == TypePopupMode::ArrayElement) {
|
} else if (mode == TypePopupMode::ArrayElement) {
|
||||||
if (entry.entryKind == TypeEntry::Primitive) {
|
if (entry.entryKind == TypeEntry::Primitive) {
|
||||||
if (entry.primitiveKind != node.elementKind) {
|
if (entry.primitiveKind != elemKind) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeArrayMeta{node.id,
|
cmd::ChangeArrayMeta{nodeId,
|
||||||
node.elementKind, entry.primitiveKind,
|
elemKind, entry.primitiveKind,
|
||||||
node.arrayLen, node.arrayLen}));
|
arrLen, arrLen}));
|
||||||
}
|
}
|
||||||
} else if (entry.entryKind == TypeEntry::Composite) {
|
} else if (entry.entryKind == TypeEntry::Composite) {
|
||||||
if (node.elementKind != NodeKind::Struct || node.refId != entry.structId) {
|
if (elemKind != NodeKind::Struct || nodeRefId != entry.structId) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeArrayMeta{node.id,
|
cmd::ChangeArrayMeta{nodeId,
|
||||||
node.elementKind, NodeKind::Struct,
|
elemKind, NodeKind::Struct,
|
||||||
node.arrayLen, node.arrayLen}));
|
arrLen, arrLen}));
|
||||||
if (node.refId != entry.structId) {
|
if (nodeRefId != entry.structId) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, node.refId, entry.structId}));
|
cmd::ChangePointerRef{nodeId, nodeRefId, entry.structId}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mode == TypePopupMode::PointerTarget) {
|
} else if (mode == TypePopupMode::PointerTarget) {
|
||||||
// "void" entry → refId 0; composite entry → real structId
|
// "void" entry → refId 0; composite entry → real structId
|
||||||
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
|
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
|
||||||
if (realRefId != node.refId) {
|
if (realRefId != nodeRefId) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, node.refId, realRefId}));
|
cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
|
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
|
||||||
@@ -1813,7 +2042,10 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
m_doc->undoStack.clear();
|
m_doc->undoStack.clear();
|
||||||
m_doc->provider = std::move(provider);
|
m_doc->provider = std::move(provider);
|
||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
|
else
|
||||||
|
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -1856,17 +2088,74 @@ void RcxController::pushSavedSourcesToEditors() {
|
|||||||
|
|
||||||
// ── Auto-refresh ──
|
// ── Auto-refresh ──
|
||||||
|
|
||||||
|
void RcxController::setRefreshInterval(int ms) {
|
||||||
|
if (m_refreshTimer)
|
||||||
|
m_refreshTimer->setInterval(qMax(1, ms));
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::setupAutoRefresh() {
|
void RcxController::setupAutoRefresh() {
|
||||||
|
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
m_refreshTimer->setInterval(2000);
|
m_refreshTimer->setInterval(qMax(1, ms));
|
||||||
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
||||||
m_refreshTimer->start();
|
m_refreshTimer->start();
|
||||||
|
|
||||||
m_refreshWatcher = new QFutureWatcher<QByteArray>(this);
|
m_refreshWatcher = new QFutureWatcher<PageMap>(this);
|
||||||
connect(m_refreshWatcher, &QFutureWatcher<QByteArray>::finished,
|
connect(m_refreshWatcher, &QFutureWatcher<PageMap>::finished,
|
||||||
this, &RcxController::onReadComplete);
|
this, &RcxController::onReadComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively collect memory ranges for a struct and its pointer targets.
|
||||||
|
// memBase is the provider-relative address where this struct's data lives.
|
||||||
|
void RcxController::collectPointerRanges(
|
||||||
|
uint64_t structId, uint64_t memBase,
|
||||||
|
int depth, int maxDepth,
|
||||||
|
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||||
|
QVector<QPair<uint64_t,int>>& ranges) const
|
||||||
|
{
|
||||||
|
if (depth >= maxDepth) return;
|
||||||
|
QPair<uint64_t,uint64_t> key{structId, memBase};
|
||||||
|
if (visited.contains(key)) return;
|
||||||
|
visited.insert(key);
|
||||||
|
|
||||||
|
int span = m_doc->tree.structSpan(structId);
|
||||||
|
if (span <= 0) return;
|
||||||
|
ranges.append({memBase, span});
|
||||||
|
|
||||||
|
if (!m_snapshotProv) return;
|
||||||
|
|
||||||
|
// Walk children looking for non-collapsed pointers
|
||||||
|
QVector<int> children = m_doc->tree.childrenOf(structId);
|
||||||
|
for (int ci : children) {
|
||||||
|
const Node& child = m_doc->tree.nodes[ci];
|
||||||
|
if (child.kind != NodeKind::Pointer32 && child.kind != NodeKind::Pointer64)
|
||||||
|
continue;
|
||||||
|
if (child.collapsed || child.refId == 0) continue;
|
||||||
|
|
||||||
|
uint64_t ptrAddr = memBase + child.offset;
|
||||||
|
int ptrSize = child.byteSize();
|
||||||
|
if (!m_snapshotProv->isReadable(ptrAddr, ptrSize)) continue;
|
||||||
|
|
||||||
|
uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
|
||||||
|
? (uint64_t)m_snapshotProv->readU32(ptrAddr)
|
||||||
|
: m_snapshotProv->readU64(ptrAddr);
|
||||||
|
if (ptrVal == 0 || ptrVal == UINT64_MAX || ptrVal < m_doc->tree.baseAddress) continue;
|
||||||
|
|
||||||
|
uint64_t pBase = ptrVal - m_doc->tree.baseAddress;
|
||||||
|
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
|
||||||
|
visited, ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded struct references (struct node with refId but no own children)
|
||||||
|
int idx = m_doc->tree.indexOfId(structId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const Node& sn = m_doc->tree.nodes[idx];
|
||||||
|
if (sn.kind == NodeKind::Struct && sn.refId != 0 && children.isEmpty())
|
||||||
|
collectPointerRanges(sn.refId, memBase, depth, maxDepth,
|
||||||
|
visited, ranges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::onRefreshTick() {
|
void RcxController::onRefreshTick() {
|
||||||
if (m_readInFlight) return;
|
if (m_readInFlight) return;
|
||||||
if (!m_doc->provider || !m_doc->provider->isLive()) return;
|
if (!m_doc->provider || !m_doc->provider->isLive()) return;
|
||||||
@@ -1877,75 +2166,120 @@ void RcxController::onRefreshTick() {
|
|||||||
int extent = computeDataExtent();
|
int extent = computeDataExtent();
|
||||||
if (extent <= 0) return;
|
if (extent <= 0) return;
|
||||||
|
|
||||||
|
// Collect all needed ranges: main struct + pointer targets
|
||||||
|
QVector<QPair<uint64_t,int>> ranges;
|
||||||
|
ranges.append({0, extent});
|
||||||
|
|
||||||
|
if (m_snapshotProv) {
|
||||||
|
QSet<QPair<uint64_t,uint64_t>> visited;
|
||||||
|
uint64_t rootId = m_viewRootId;
|
||||||
|
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
|
||||||
|
rootId = m_doc->tree.nodes[0].id;
|
||||||
|
collectPointerRanges(rootId, 0, 0, 99, visited, ranges);
|
||||||
|
}
|
||||||
|
|
||||||
m_readInFlight = true;
|
m_readInFlight = true;
|
||||||
m_readGen = m_refreshGen;
|
m_readGen = m_refreshGen;
|
||||||
|
|
||||||
// Capture shared_ptr copy — keeps provider alive during async read
|
|
||||||
auto prov = m_doc->provider;
|
auto prov = m_doc->provider;
|
||||||
uint64_t base = prov->base();
|
qDebug() << "[Refresh] reading" << ranges.size() << "ranges from base"
|
||||||
qDebug() << "[Refresh] reading" << extent << "bytes from base" << Qt::hex << base;
|
<< Qt::hex << prov->base();
|
||||||
m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray {
|
m_refreshWatcher->setFuture(QtConcurrent::run([prov, ranges]() -> PageMap {
|
||||||
return prov->readBytes(0, extent);
|
constexpr uint64_t kPageSize = 4096;
|
||||||
|
constexpr uint64_t kPageMask = ~(kPageSize - 1);
|
||||||
|
PageMap pages;
|
||||||
|
for (const auto& r : ranges) {
|
||||||
|
uint64_t pageStart = r.first & kPageMask;
|
||||||
|
uint64_t end = r.first + r.second;
|
||||||
|
uint64_t pageEnd = (end + kPageSize - 1) & kPageMask;
|
||||||
|
for (uint64_t p = pageStart; p < pageEnd; p += kPageSize) {
|
||||||
|
if (!pages.contains(p))
|
||||||
|
pages[p] = prov->readBytes(p, static_cast<int>(kPageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::onReadComplete() {
|
void RcxController::onReadComplete() {
|
||||||
m_readInFlight = false;
|
m_readInFlight = false;
|
||||||
|
|
||||||
// Stale read (provider changed while we were reading) — discard
|
|
||||||
if (m_readGen != m_refreshGen) return;
|
if (m_readGen != m_refreshGen) return;
|
||||||
|
|
||||||
QByteArray newData = m_refreshWatcher->result();
|
PageMap newPages;
|
||||||
|
try {
|
||||||
|
newPages = m_refreshWatcher->result();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
qWarning() << "[Refresh] async read threw:" << e.what();
|
||||||
|
return;
|
||||||
|
} catch (...) {
|
||||||
|
qWarning() << "[Refresh] async read threw unknown exception";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fast path: no changes at all — skip full recompose
|
// All-zero guard: if page 0 is all zeros and we already have data, discard
|
||||||
if (!m_prevSnapshot.isEmpty() && m_prevSnapshot.size() == newData.size()
|
if (!m_prevPages.isEmpty() && newPages.contains(0)) {
|
||||||
&& memcmp(m_prevSnapshot.constData(), newData.constData(), newData.size()) == 0)
|
const QByteArray& p0 = newPages.value(0);
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < p0.size(); ++i) {
|
||||||
|
if (p0[i] != 0) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
if (allZero) {
|
||||||
|
qDebug() << "[Refresh] discarding all-zero page-0, keeping stale snapshot";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path: no changes at all
|
||||||
|
if (newPages == m_prevPages)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Compute which byte offsets changed
|
// Compute which byte offsets changed (for change highlighting).
|
||||||
|
// Skip on first snapshot — nothing to compare against.
|
||||||
m_changedOffsets.clear();
|
m_changedOffsets.clear();
|
||||||
if (!m_prevSnapshot.isEmpty()) {
|
if (!m_prevPages.isEmpty()) {
|
||||||
int compareLen = qMin(m_prevSnapshot.size(), newData.size());
|
for (auto it = newPages.constBegin(); it != newPages.constEnd(); ++it) {
|
||||||
const char* oldP = m_prevSnapshot.constData();
|
uint64_t pageAddr = it.key();
|
||||||
const char* newP = newData.constData();
|
const QByteArray& newPage = it.value();
|
||||||
for (int i = 0; i < compareLen; i++) {
|
auto oldIt = m_prevPages.constFind(pageAddr);
|
||||||
if (oldP[i] != newP[i])
|
if (oldIt == m_prevPages.constEnd())
|
||||||
m_changedOffsets.insert(i);
|
continue; // new page, no previous data to diff against
|
||||||
|
const QByteArray& oldPage = oldIt.value();
|
||||||
|
int cmpLen = qMin(oldPage.size(), newPage.size());
|
||||||
|
for (int i = 0; i < cmpLen; ++i) {
|
||||||
|
if (oldPage[i] != newPage[i])
|
||||||
|
m_changedOffsets.insert(static_cast<int64_t>(pageAddr) + i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Bytes beyond old snapshot are all "new"
|
|
||||||
for (int i = compareLen; i < newData.size(); i++)
|
|
||||||
m_changedOffsets.insert(i);
|
|
||||||
}
|
}
|
||||||
m_prevSnapshot = newData;
|
|
||||||
|
|
||||||
// Update or create snapshot provider
|
int mainExtent = computeDataExtent();
|
||||||
|
m_prevPages = newPages;
|
||||||
|
|
||||||
if (m_snapshotProv)
|
if (m_snapshotProv)
|
||||||
m_snapshotProv->updateSnapshot(std::move(newData));
|
m_snapshotProv->updatePages(std::move(newPages), mainExtent);
|
||||||
else
|
else
|
||||||
m_snapshotProv = std::make_unique<SnapshotProvider>(m_doc->provider, std::move(newData));
|
m_snapshotProv = std::make_unique<SnapshotProvider>(
|
||||||
|
m_doc->provider, std::move(newPages), mainExtent);
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
// Clear changed offsets after refresh consumed them
|
|
||||||
m_changedOffsets.clear();
|
m_changedOffsets.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
int RcxController::computeDataExtent() const {
|
int RcxController::computeDataExtent() const {
|
||||||
// Prefer tree-based extent: exact bytes needed for rendering
|
static constexpr int64_t kMaxMainExtent = 16 * 1024 * 1024; // 16 MB cap
|
||||||
|
|
||||||
int64_t treeExtent = 0;
|
int64_t treeExtent = 0;
|
||||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||||
const Node& node = m_doc->tree.nodes[i];
|
const Node& node = m_doc->tree.nodes[i];
|
||||||
int64_t off = m_doc->tree.computeOffset(i);
|
int64_t off = m_doc->tree.computeOffset(i);
|
||||||
// byteSize() returns 0 for Array-of-Struct/Array; use structSpan() for containers
|
|
||||||
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
||||||
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
||||||
int64_t end = off + sz;
|
int64_t end = off + sz;
|
||||||
if (end > treeExtent) treeExtent = end;
|
if (end > treeExtent) treeExtent = end;
|
||||||
}
|
}
|
||||||
// Clamp to max int (readBytes takes int length)
|
if (treeExtent > 0) return static_cast<int>(qMin(treeExtent, kMaxMainExtent));
|
||||||
if (treeExtent > 0) return (int)qMin(treeExtent, (int64_t)std::numeric_limits<int>::max());
|
|
||||||
|
|
||||||
// Fallback: provider size (empty tree)
|
|
||||||
int provSize = m_doc->provider->size();
|
int provSize = m_doc->provider->size();
|
||||||
if (provSize > 0) return provSize;
|
if (provSize > 0) return provSize;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1955,8 +2289,9 @@ void RcxController::resetSnapshot() {
|
|||||||
m_refreshGen++;
|
m_refreshGen++;
|
||||||
m_readInFlight = false;
|
m_readInFlight = false;
|
||||||
m_snapshotProv.reset();
|
m_snapshotProv.reset();
|
||||||
m_prevSnapshot.clear();
|
m_prevPages.clear();
|
||||||
m_changedOffsets.clear();
|
m_changedOffsets.clear();
|
||||||
|
m_valueHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
||||||
@@ -1965,6 +2300,9 @@ void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
|||||||
if (!lm) return;
|
if (!lm) return;
|
||||||
|
|
||||||
if (lm->foldHead && (margin == 0 || margin == 1)) {
|
if (lm->foldHead && (margin == 0 || margin == 1)) {
|
||||||
|
if (lm->markerMask & (1u << M_CYCLE))
|
||||||
|
materializeRefChildren(lm->nodeIdx);
|
||||||
|
else
|
||||||
toggleCollapse(lm->nodeIdx);
|
toggleCollapse(lm->nodeIdx);
|
||||||
} else if (margin == 0 || margin == 1) {
|
} else if (margin == 0 || margin == 1) {
|
||||||
emit nodeSelected(lm->nodeIdx);
|
emit nodeSelected(lm->nodeIdx);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QUndoCommand>
|
#include <QUndoCommand>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#include <QPointer>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -89,6 +90,7 @@ public:
|
|||||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||||
void removeNode(int nodeIdx);
|
void removeNode(int nodeIdx);
|
||||||
void toggleCollapse(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);
|
||||||
void duplicateNode(int nodeIdx);
|
void duplicateNode(int nodeIdx);
|
||||||
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||||
@@ -111,6 +113,7 @@ public:
|
|||||||
|
|
||||||
RcxDocument* document() const { return m_doc; }
|
RcxDocument* document() const { return m_doc; }
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
|
void setRefreshInterval(int ms);
|
||||||
|
|
||||||
// MCP bridge accessors
|
// MCP bridge accessors
|
||||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||||
@@ -119,6 +122,9 @@ public:
|
|||||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||||
|
|
||||||
|
// Test accessor
|
||||||
|
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
void selectionChanged(int count);
|
void selectionChanged(int count);
|
||||||
@@ -137,14 +143,16 @@ private:
|
|||||||
int m_activeSourceIdx = -1;
|
int m_activeSourceIdx = -1;
|
||||||
|
|
||||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||||
TypeSelectorPopup* m_cachedPopup = nullptr;
|
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||||
|
|
||||||
// ── Auto-refresh state ──
|
// ── Auto-refresh state ──
|
||||||
|
using PageMap = QHash<uint64_t, QByteArray>;
|
||||||
QTimer* m_refreshTimer = nullptr;
|
QTimer* m_refreshTimer = nullptr;
|
||||||
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
|
||||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||||
QByteArray m_prevSnapshot;
|
PageMap m_prevPages;
|
||||||
QSet<int64_t> m_changedOffsets;
|
QSet<int64_t> m_changedOffsets;
|
||||||
|
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||||
uint64_t m_refreshGen = 0;
|
uint64_t m_refreshGen = 0;
|
||||||
uint64_t m_readGen = 0;
|
uint64_t m_readGen = 0;
|
||||||
bool m_readInFlight = false;
|
bool m_readInFlight = false;
|
||||||
@@ -165,6 +173,10 @@ private:
|
|||||||
void onReadComplete();
|
void onReadComplete();
|
||||||
int computeDataExtent() const;
|
int computeDataExtent() const;
|
||||||
void resetSnapshot();
|
void resetSnapshot();
|
||||||
|
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
||||||
|
int depth, int maxDepth,
|
||||||
|
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||||
|
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
79
src/core.h
79
src/core.h
@@ -8,6 +8,7 @@
|
|||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
@@ -27,21 +28,20 @@ enum class NodeKind : uint8_t {
|
|||||||
Pointer32, Pointer64,
|
Pointer32, Pointer64,
|
||||||
Vec2, Vec3, Vec4, Mat4x4,
|
Vec2, Vec3, Vec4, Mat4x4,
|
||||||
UTF8, UTF16,
|
UTF8, UTF16,
|
||||||
Padding,
|
|
||||||
Struct, Array
|
Struct, Array
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx (temporarily close for qHash)
|
} // namespace rcx (temporarily close for qHash)
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
#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
|
#endif
|
||||||
namespace rcx { // reopen
|
namespace rcx { // reopen
|
||||||
|
|
||||||
// ── Kind flags (replaces repeated Hex/Padding switches) ──
|
// ── Kind flags (replaces repeated Hex switches) ──
|
||||||
|
|
||||||
enum KindFlags : uint32_t {
|
enum KindFlags : uint32_t {
|
||||||
KF_None = 0,
|
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_Container = 1 << 1, // Struct/Array
|
||||||
KF_String = 1 << 2, // UTF8/UTF16
|
KF_String = 1 << 2, // UTF8/UTF16
|
||||||
KF_Vector = 1 << 3, // Vec2/3/4
|
KF_Vector = 1 << 3, // Vec2/3/4
|
||||||
@@ -84,7 +84,6 @@ inline constexpr KindMeta kKindMeta[] = {
|
|||||||
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
|
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
|
||||||
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
|
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
|
||||||
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, 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::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||||
};
|
};
|
||||||
@@ -155,7 +154,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
|||||||
|
|
||||||
enum Marker : int {
|
enum Marker : int {
|
||||||
M_CONT = 0,
|
M_CONT = 0,
|
||||||
M_PAD = 1,
|
|
||||||
M_PTR0 = 2,
|
M_PTR0 = 2,
|
||||||
M_CYCLE = 3,
|
M_CYCLE = 3,
|
||||||
M_ERR = 4,
|
M_ERR = 4,
|
||||||
@@ -187,9 +185,12 @@ struct Node {
|
|||||||
int byteSize() const {
|
int byteSize() const {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case NodeKind::UTF8: return strLen;
|
case NodeKind::UTF8: return strLen;
|
||||||
case NodeKind::UTF16: return strLen * 2;
|
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
|
||||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
case NodeKind::Array: {
|
||||||
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
int elemSz = sizeForKind(elementKind);
|
||||||
|
if (elemSz <= 0) return 0;
|
||||||
|
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||||
|
}
|
||||||
default: return sizeForKind(kind);
|
default: return sizeForKind(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,8 +222,8 @@ struct Node {
|
|||||||
n.classKeyword = o["classKeyword"].toString();
|
n.classKeyword = o["classKeyword"].toString();
|
||||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||||
n.offset = o["offset"].toInt(0);
|
n.offset = o["offset"].toInt(0);
|
||||||
n.arrayLen = o["arrayLen"].toInt(1);
|
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||||
n.strLen = o["strLen"].toInt(64);
|
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
|
||||||
n.collapsed = o["collapsed"].toBool(false);
|
n.collapsed = o["collapsed"].toBool(false);
|
||||||
n.refId = o["refId"].toString("0").toULongLong();
|
n.refId = o["refId"].toString("0").toULongLong();
|
||||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||||
@@ -405,6 +406,49 @@ struct NodeTree {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Value History (ring buffer for heatmap) ──
|
||||||
|
|
||||||
|
struct ValueHistory {
|
||||||
|
static constexpr int kCapacity = 10;
|
||||||
|
std::array<QString, kCapacity> values;
|
||||||
|
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;
|
||||||
|
head = (head + 1) % kCapacity;
|
||||||
|
if (count < INT_MAX) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── LineMeta ──
|
// ── LineMeta ──
|
||||||
|
|
||||||
enum class LineKind : uint8_t {
|
enum class LineKind : uint8_t {
|
||||||
@@ -439,6 +483,7 @@ struct LineMeta {
|
|||||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||||
uint32_t markerMask = 0;
|
uint32_t markerMask = 0;
|
||||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
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
|
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 lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||||
@@ -535,7 +580,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
|||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int start = ind + typeW + kSepWidth;
|
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))
|
if (isHexPreview(lm.nodeKind))
|
||||||
return {start, start + nameW, true};
|
return {start, start + nameW, true};
|
||||||
|
|
||||||
@@ -547,9 +592,9 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
|||||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
// Hex/Padding uses nameW for ASCII column (same as regular name column)
|
// Hex uses nameW for ASCII column (same as regular name column)
|
||||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
bool isHex = isHexPreview(lm.nodeKind);
|
||||||
int valWidth = isHexPad ? 23 : kColValue;
|
int valWidth = isHex ? 23 : kColValue;
|
||||||
|
|
||||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||||
|
|
||||||
@@ -567,8 +612,8 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
|||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
bool isHex = isHexPreview(lm.nodeKind);
|
||||||
int valWidth = isHexPad ? 23 : kColValue;
|
int valWidth = isHex ? 23 : kColValue;
|
||||||
|
|
||||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||||
int start;
|
int start;
|
||||||
|
|||||||
370
src/editor.cpp
370
src/editor.cpp
@@ -5,6 +5,7 @@
|
|||||||
#include <Qsci/qsciscintillabase.h>
|
#include <Qsci/qsciscintillabase.h>
|
||||||
#include <Qsci/qscilexercpp.h>
|
#include <Qsci/qscilexercpp.h>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
@@ -14,19 +15,154 @@
|
|||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <functional>
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
// ── Value history popup (styled like TypeSelectorPopup) ──
|
||||||
|
|
||||||
|
class ValueHistoryPopup : public QFrame {
|
||||||
|
uint64_t m_nodeId = 0;
|
||||||
|
bool m_hasButtons = false;
|
||||||
|
QStringList m_values;
|
||||||
|
QVector<QLabel*> m_labels;
|
||||||
|
std::function<void(const QString&)> m_onSet;
|
||||||
|
public:
|
||||||
|
explicit ValueHistoryPopup(QWidget* parent)
|
||||||
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setFrameShape(QFrame::NoFrame);
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t nodeId() const { return m_nodeId; }
|
||||||
|
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
||||||
|
|
||||||
|
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
||||||
|
bool showButtons = false) {
|
||||||
|
QStringList vals;
|
||||||
|
hist.forEach([&](const QString& v) { vals.append(v); });
|
||||||
|
|
||||||
|
if (nodeId == m_nodeId && vals == m_values
|
||||||
|
&& showButtons == m_hasButtons && isVisible())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// In-place label update when structure unchanged (avoids flicker)
|
||||||
|
if (nodeId == m_nodeId && vals.size() == m_values.size()
|
||||||
|
&& vals.size() == m_labels.size()
|
||||||
|
&& showButtons == m_hasButtons && isVisible()) {
|
||||||
|
for (int i = 0; i < vals.size(); i++)
|
||||||
|
m_labels[i]->setText(vals[i]);
|
||||||
|
m_values = vals;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_nodeId = nodeId;
|
||||||
|
m_values = vals;
|
||||||
|
m_hasButtons = showButtons;
|
||||||
|
m_labels.clear();
|
||||||
|
|
||||||
|
delete layout();
|
||||||
|
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
|
||||||
|
|
||||||
|
const auto& theme = ThemeManager::instance().current();
|
||||||
|
QPalette pal;
|
||||||
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||||
|
pal.setColor(QPalette::WindowText, theme.text);
|
||||||
|
setPalette(pal);
|
||||||
|
|
||||||
|
auto* vbox = new QVBoxLayout(this);
|
||||||
|
vbox->setContentsMargins(8, 6, 8, 6);
|
||||||
|
vbox->setSpacing(2);
|
||||||
|
|
||||||
|
auto* title = new QLabel(QStringLiteral("Previous Values"));
|
||||||
|
QFont bold = font;
|
||||||
|
bold.setBold(true);
|
||||||
|
title->setFont(bold);
|
||||||
|
title->setStyleSheet(QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||||
|
vbox->addWidget(title);
|
||||||
|
|
||||||
|
auto* sep = new QFrame;
|
||||||
|
sep->setFrameShape(QFrame::HLine);
|
||||||
|
sep->setFrameShadow(QFrame::Plain);
|
||||||
|
sep->setFixedHeight(1);
|
||||||
|
QPalette sp; sp.setColor(QPalette::WindowText, theme.border);
|
||||||
|
sep->setPalette(sp);
|
||||||
|
vbox->addWidget(sep);
|
||||||
|
|
||||||
|
for (const QString& v : vals) {
|
||||||
|
auto* row = new QHBoxLayout;
|
||||||
|
row->setContentsMargins(0, 1, 0, 1);
|
||||||
|
row->setSpacing(8);
|
||||||
|
|
||||||
|
auto* label = new QLabel(v);
|
||||||
|
label->setFont(font);
|
||||||
|
label->setStyleSheet(QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
||||||
|
row->addWidget(label, 1);
|
||||||
|
m_labels.append(label);
|
||||||
|
|
||||||
|
if (showButtons) {
|
||||||
|
auto* setBtn = new QToolButton;
|
||||||
|
setBtn->setText(QStringLiteral("Set"));
|
||||||
|
setBtn->setAutoRaise(true);
|
||||||
|
setBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
setBtn->setFont(font);
|
||||||
|
setBtn->setStyleSheet(QStringLiteral(
|
||||||
|
"QToolButton { color: %1; border: none; padding: 1px 4px; }"
|
||||||
|
"QToolButton:hover { color: %2; background: %3; }")
|
||||||
|
.arg(theme.textDim.name(), theme.text.name(), theme.hover.name()));
|
||||||
|
QString val = v;
|
||||||
|
QObject::connect(setBtn, &QToolButton::clicked, [this, val]() {
|
||||||
|
if (m_onSet) m_onSet(val);
|
||||||
|
});
|
||||||
|
row->addWidget(setBtn);
|
||||||
|
}
|
||||||
|
vbox->addLayout(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAt(const QPoint& globalPos) {
|
||||||
|
if (isVisible()) return;
|
||||||
|
QSize sz = sizeHint();
|
||||||
|
QRect screen = QApplication::screenAt(globalPos)
|
||||||
|
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||||
|
: QRect(0, 0, 1920, 1080);
|
||||||
|
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||||
|
int y = globalPos.y();
|
||||||
|
if (y + sz.height() > screen.bottom())
|
||||||
|
y = globalPos.y() - sz.height() - 4;
|
||||||
|
move(x, y);
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismiss() {
|
||||||
|
if (isVisible()) hide();
|
||||||
|
m_nodeId = 0;
|
||||||
|
m_values.clear();
|
||||||
|
m_labels.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static constexpr int IND_EDITABLE = 8;
|
static constexpr int IND_EDITABLE = 8;
|
||||||
static constexpr int IND_HEX_DIM = 9;
|
static constexpr int IND_HEX_DIM = 9;
|
||||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||||
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||||
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
||||||
static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values
|
static constexpr int IND_HEAT_COLD = 13; // Heatmap level 1 (changed once)
|
||||||
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
|
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
|
||||||
static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
||||||
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
||||||
|
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||||
|
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||||
|
|
||||||
static QString g_fontName = "JetBrains Mono";
|
static QString g_fontName = "JetBrains Mono";
|
||||||
|
|
||||||
@@ -68,6 +204,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
|
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
connect(m_sci, &QWidget::customContextMenuRequested,
|
connect(m_sci, &QWidget::customContextMenuRequested,
|
||||||
this, [this](const QPoint& pos) {
|
this, [this](const QPoint& pos) {
|
||||||
|
// Right-click on offset margin → show margin mode menu
|
||||||
|
int margin0Width = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
||||||
|
if (pos.x() < margin0Width) {
|
||||||
|
QMenu menu;
|
||||||
|
auto* actRel = menu.addAction("Relative Offsets (+0x)");
|
||||||
|
auto* actAbs = menu.addAction("Absolute Addresses");
|
||||||
|
actRel->setCheckable(true);
|
||||||
|
actAbs->setCheckable(true);
|
||||||
|
actRel->setChecked(m_relativeOffsets);
|
||||||
|
actAbs->setChecked(!m_relativeOffsets);
|
||||||
|
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
|
||||||
|
if (chosen == actRel && !m_relativeOffsets) {
|
||||||
|
m_relativeOffsets = true;
|
||||||
|
reformatMargins();
|
||||||
|
} else if (chosen == actAbs && m_relativeOffsets) {
|
||||||
|
m_relativeOffsets = false;
|
||||||
|
reformatMargins();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
int line = m_sci->lineAt(pos);
|
int line = m_sci->lineAt(pos);
|
||||||
int nodeIdx = -1;
|
int nodeIdx = -1;
|
||||||
int subLine = 0;
|
int subLine = 0;
|
||||||
@@ -140,7 +297,7 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
||||||
|
|
||||||
// Hex/Padding node dim indicator — overrides text color
|
// Hex node dim indicator — overrides text color
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
|
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
|
||||||
@@ -160,9 +317,13 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||||
IND_CMD_PILL, (long)1);
|
IND_CMD_PILL, (long)1);
|
||||||
|
|
||||||
// Data-changed indicator
|
// Heatmap indicators (cold / warm / hot)
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
IND_HEAT_COLD, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_HEAT_WARM, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_HEAT_HOT, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
|
||||||
// Root class name — type color
|
// Root class name — type color
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
@@ -240,9 +401,6 @@ void RcxEditor::setupMarkers() {
|
|||||||
// M_CONT (0): continuation line (metadata only, no visual)
|
// M_CONT (0): continuation line (metadata only, no visual)
|
||||||
m_sci->markerDefine(QsciScintilla::Invisible, M_CONT);
|
m_sci->markerDefine(QsciScintilla::Invisible, M_CONT);
|
||||||
|
|
||||||
// M_PAD (1): padding line (metadata only, no visual)
|
|
||||||
m_sci->markerDefine(QsciScintilla::Invisible, M_PAD);
|
|
||||||
|
|
||||||
// M_PTR0 (2): right triangle
|
// M_PTR0 (2): right triangle
|
||||||
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
|
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
|
||||||
|
|
||||||
@@ -302,8 +460,13 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
IND_HOVER_SPAN, theme.indHoverSpan);
|
IND_HOVER_SPAN, theme.indHoverSpan);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_CMD_PILL, theme.indCmdPill);
|
IND_CMD_PILL, theme.indCmdPill);
|
||||||
|
// Heatmap colors
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_DATA_CHANGED, theme.indDataChanged);
|
IND_HEAT_COLD, theme.indHeatCold);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_HEAT_WARM, theme.indHeatWarm);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_HEAT_HOT, theme.indHeatHot);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_CLASS_NAME, theme.syntaxType);
|
IND_CLASS_NAME, theme.syntaxType);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
@@ -365,6 +528,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
if (m_editState.active)
|
if (m_editState.active)
|
||||||
endInlineEdit();
|
endInlineEdit();
|
||||||
|
|
||||||
|
// Guard: suppress popup dismiss during setText() which fires synthetic Leave events
|
||||||
|
m_applyingDocument = true;
|
||||||
|
|
||||||
// Save hover state — setText() triggers viewport Leave events that would clear it
|
// Save hover state — setText() triggers viewport Leave events that would clear it
|
||||||
uint64_t savedHoverId = m_hoveredNodeId;
|
uint64_t savedHoverId = m_hoveredNodeId;
|
||||||
int savedHoverLine = m_hoveredLine;
|
int savedHoverLine = m_hoveredLine;
|
||||||
@@ -403,7 +569,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
applyMarkers(result.meta);
|
applyMarkers(result.meta);
|
||||||
applyFoldLevels(result.meta);
|
applyFoldLevels(result.meta);
|
||||||
applyHexDimming(result.meta);
|
applyHexDimming(result.meta);
|
||||||
applyDataChangedHighlight(result.meta);
|
applyHeatmapHighlight(result.meta);
|
||||||
applyCommandRowPills();
|
applyCommandRowPills();
|
||||||
|
|
||||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
@@ -413,6 +579,13 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
m_hoveredNodeId = savedHoverId;
|
m_hoveredNodeId = savedHoverId;
|
||||||
m_hoveredLine = savedHoverLine;
|
m_hoveredLine = savedHoverLine;
|
||||||
m_hoverInside = savedHoverInside;
|
m_hoverInside = savedHoverInside;
|
||||||
|
m_applyingDocument = false;
|
||||||
|
|
||||||
|
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||||
|
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
||||||
|
// composed text that updateCommandRow() will overwrite. The correct call
|
||||||
|
// happens via applySelectionOverlays() after all text is finalized.
|
||||||
|
applyHoverHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||||
@@ -767,35 +940,52 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::applyDataChangedHighlight(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||||
for (int i = 0; i < meta.size(); i++) {
|
static constexpr int heatIndicators[] = { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT };
|
||||||
if (!meta[i].dataChanged) continue;
|
|
||||||
if (isSyntheticLine(meta[i])) continue;
|
|
||||||
|
|
||||||
|
for (int i = 0; i < meta.size(); i++) {
|
||||||
const LineMeta& lm = meta[i];
|
const LineMeta& lm = meta[i];
|
||||||
|
if (isSyntheticLine(lm)) continue;
|
||||||
|
|
||||||
|
int heat = lm.heatLevel;
|
||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int nameW = lm.effectiveNameW;
|
int nameW = lm.effectiveNameW;
|
||||||
|
|
||||||
if (isHexPreview(lm.nodeKind) && !lm.changedByteIndices.isEmpty()) {
|
if (heat <= 0) continue;
|
||||||
// Per-byte highlighting in ASCII + hex areas
|
|
||||||
|
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||||
|
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||||
|
|
||||||
|
// For hex preview nodes: per-byte heat coloring on changed bytes
|
||||||
|
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int asciiStart = ind + typeW + kSepWidth;
|
int asciiStart = ind + typeW + kSepWidth;
|
||||||
// ASCII column is padded to nameW (aligned with value column)
|
|
||||||
int hexStart = asciiStart + nameW + kSepWidth;
|
int hexStart = asciiStart + nameW + kSepWidth;
|
||||||
|
|
||||||
for (int byteIdx : lm.changedByteIndices) {
|
for (int byteIdx : lm.changedByteIndices) {
|
||||||
// Highlight in ASCII area (1 char per byte)
|
fillIndicatorCols(activeInd, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
|
||||||
// Highlight in hex area (2 hex chars per byte at position byteIdx*3)
|
|
||||||
int hexCol = hexStart + byteIdx * 3;
|
int hexCol = hexStart + byteIdx * 3;
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, hexCol, hexCol + 2);
|
fillIndicatorCols(activeInd, i, hexCol, hexCol + 2);
|
||||||
}
|
}
|
||||||
} else {
|
// Clear the other two heat indicators on this line
|
||||||
// Non-hex nodes: highlight entire value span
|
for (int hi : heatIndicators) {
|
||||||
|
if (hi != activeInd)
|
||||||
|
clearIndicatorLine(hi, i);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-hex nodes: apply heat-level indicator to value span
|
||||||
QString lineText = getLineText(m_sci, i);
|
QString lineText = getLineText(m_sci, i);
|
||||||
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
||||||
if (vs.valid)
|
if (!vs.valid) continue;
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
|
||||||
|
fillIndicatorCols(activeInd, i, vs.start, vs.end);
|
||||||
|
|
||||||
|
// Clear the other two heat indicators on this span to avoid overlap
|
||||||
|
for (int hi : heatIndicators) {
|
||||||
|
if (hi != activeInd)
|
||||||
|
clearIndicatorLine(hi, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1037,9 +1227,6 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|||||||
|
|
||||||
if (lm->nodeIdx < 0) return false;
|
if (lm->nodeIdx < 0) return false;
|
||||||
|
|
||||||
// Padding: reject value editing (hex bytes are display-only)
|
|
||||||
if (t == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
|
||||||
return false;
|
|
||||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||||
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||||
return false;
|
return false;
|
||||||
@@ -1220,9 +1407,6 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Padding nodes: hex bytes are display-only, not editable
|
|
||||||
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
|
|
||||||
return false;
|
|
||||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||||
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
|
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
|
||||||
return false;
|
return false;
|
||||||
@@ -1329,7 +1513,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
// Single-click on editable token of already-selected node → edit
|
// Single-click on editable token of already-selected node → edit
|
||||||
int tLine, tCol; EditTarget t;
|
int tLine, tCol; EditTarget t;
|
||||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
||||||
if (alreadySelected && plain) {
|
// Type/ArrayElementType/PointerTarget open a dismissible popup
|
||||||
|
// (not inline text edit), so allow on first click without
|
||||||
|
// requiring the node to be pre-selected.
|
||||||
|
bool isPopupTarget = (t == EditTarget::Type
|
||||||
|
|| t == EditTarget::ArrayElementType
|
||||||
|
|| t == EditTarget::PointerTarget);
|
||||||
|
if ((alreadySelected || isPopupTarget) && plain) {
|
||||||
|
if (!alreadySelected)
|
||||||
|
emit nodeClicked(h.line, h.nodeId, me->modifiers());
|
||||||
m_pendingClickNodeId = 0;
|
m_pendingClickNodeId = 0;
|
||||||
return beginInlineEdit(t, tLine, tCol);
|
return beginInlineEdit(t, tLine, tCol);
|
||||||
}
|
}
|
||||||
@@ -1401,7 +1593,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
int margin0Width = (int)m_sci->SendScintilla(
|
int margin0Width = (int)m_sci->SendScintilla(
|
||||||
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
if ((int)me->position().x() < margin0Width) {
|
if ((int)me->position().x() < margin0Width) {
|
||||||
|
#else
|
||||||
|
if ((int)me->pos().x() < margin0Width) {
|
||||||
|
#endif
|
||||||
m_relativeOffsets = !m_relativeOffsets;
|
m_relativeOffsets = !m_relativeOffsets;
|
||||||
reformatMargins();
|
reformatMargins();
|
||||||
return true;
|
return true;
|
||||||
@@ -1450,6 +1646,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
}
|
}
|
||||||
// Track mouse position for cursor updates (both edit and non-edit mode)
|
// Track mouse position for cursor updates (both edit and non-edit mode)
|
||||||
if (obj == m_sci->viewport()) {
|
if (obj == m_sci->viewport()) {
|
||||||
|
// Ignore synthetic Leave from setText() during document refresh
|
||||||
|
if (m_applyingDocument && event->type() == QEvent::Leave)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (event->type() == QEvent::MouseMove) {
|
if (event->type() == QEvent::MouseMove) {
|
||||||
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
|
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
|
||||||
m_hoverInside = true;
|
m_hoverInside = true;
|
||||||
@@ -1603,6 +1803,22 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
case Qt::Key_End:
|
case Qt::Key_End:
|
||||||
m_sci->setCursorPosition(m_editState.line, editEndCol());
|
m_sci->setCursorPosition(m_editState.line, editEndCol());
|
||||||
return true;
|
return true;
|
||||||
|
case Qt::Key_V:
|
||||||
|
if (ke->modifiers() & Qt::ControlModifier) {
|
||||||
|
// Sanitized paste: strip newlines (and backticks for base addresses)
|
||||||
|
QString clip = QApplication::clipboard()->text();
|
||||||
|
clip.remove('\n');
|
||||||
|
clip.remove('\r');
|
||||||
|
if (m_editState.target == EditTarget::BaseAddress)
|
||||||
|
clip.remove('`');
|
||||||
|
if (!clip.isEmpty()) {
|
||||||
|
QByteArray utf8 = clip.toUtf8();
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||||
|
(uintptr_t)0, utf8.constData());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1639,6 +1855,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
m_hoveredLine = -1;
|
m_hoveredLine = -1;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
|
// Dismiss hover popup so it gets recreated with Set buttons once edit starts
|
||||||
|
if (m_historyPopup)
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
@@ -1656,9 +1875,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
(target == EditTarget::BaseAddress || target == EditTarget::Source
|
(target == EditTarget::BaseAddress || target == EditTarget::Source
|
||||||
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
|
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
|
||||||
return false;
|
return false;
|
||||||
// Padding: reject value editing (display-only hex bytes)
|
|
||||||
if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
|
||||||
return false;
|
|
||||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||||
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||||
return false;
|
return false;
|
||||||
@@ -1823,6 +2039,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Refresh hover cursor so value history popup appears with Set buttons immediately
|
||||||
|
if (target == EditTarget::Value)
|
||||||
|
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1961,7 +2180,7 @@ void RcxEditor::showSourcePicker() {
|
|||||||
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
||||||
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
||||||
menu.setFont(menuFont);
|
menu.setFont(menuFont);
|
||||||
menu.addAction("file");
|
menu.addAction("File");
|
||||||
|
|
||||||
// Add all registered providers from global registry
|
// Add all registered providers from global registry
|
||||||
const auto& providers = ProviderRegistry::instance().providers();
|
const auto& providers = ProviderRegistry::instance().providers();
|
||||||
@@ -2175,8 +2394,7 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
if (m_editState.active) {
|
if (m_editState.active) {
|
||||||
if (m_sci->isListActive()) {
|
if (m_sci->isListActive()) {
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
auto h = hitTest(m_lastHoverPos);
|
auto h = hitTest(m_lastHoverPos);
|
||||||
if (h.line == m_editState.line &&
|
if (h.line == m_editState.line &&
|
||||||
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
||||||
@@ -2184,11 +2402,52 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
} else {
|
} else {
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Value history popup — only during inline value editing on a heated node
|
||||||
|
{
|
||||||
|
bool showPopup = false;
|
||||||
|
if (m_valueHistory && m_editState.target == EditTarget::Value
|
||||||
|
&& m_editState.line >= 0 && m_editState.line < m_meta.size()) {
|
||||||
|
const LineMeta& lm = m_meta[m_editState.line];
|
||||||
|
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||||
|
auto it = m_valueHistory->find(lm.nodeId);
|
||||||
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||||
|
if (!m_historyPopup)
|
||||||
|
m_historyPopup = new ValueHistoryPopup(this);
|
||||||
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||||
|
popup->setOnSet([this](const QString& val) {
|
||||||
|
if (!m_editState.active) return;
|
||||||
|
long endPos = posFromCol(m_sci, m_editState.line, editEndCol());
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
||||||
|
m_editState.posStart, endPos);
|
||||||
|
QByteArray utf8 = val.toUtf8();
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||||
|
(uintptr_t)0, utf8.constData());
|
||||||
|
});
|
||||||
|
popup->populate(lm.nodeId, *it, editorFont(), true);
|
||||||
|
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||||
|
(unsigned long)0, m_editState.posStart);
|
||||||
|
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||||
|
(unsigned long)0, m_editState.posStart);
|
||||||
|
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||||
|
(unsigned long)m_editState.line);
|
||||||
|
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
||||||
|
popup->showAt(anchor);
|
||||||
|
showPopup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse left viewport - set Arrow
|
// Mouse left viewport - set Arrow, dismiss history popup
|
||||||
|
// (but not during applyDocument — the Leave is synthetic from setText)
|
||||||
if (!m_hoverInside) {
|
if (!m_hoverInside) {
|
||||||
|
if (m_historyPopup && !m_applyingDocument)
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2277,6 +2536,41 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
m_hoverSpanLines.append(h.line);
|
m_hoverSpanLines.append(h.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Value history popup on hover (read-only, no buttons)
|
||||||
|
{
|
||||||
|
bool showPopup = false;
|
||||||
|
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
|
||||||
|
const LineMeta& lm = m_meta[h.line];
|
||||||
|
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||||
|
auto it = m_valueHistory->find(lm.nodeId);
|
||||||
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||||
|
QString lineText = getLineText(m_sci, h.line);
|
||||||
|
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||||
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||||
|
if (!m_historyPopup)
|
||||||
|
m_historyPopup = new ValueHistoryPopup(this);
|
||||||
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||||
|
popup->populate(lm.nodeId, *it, editorFont(), false);
|
||||||
|
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
|
(unsigned long)h.line);
|
||||||
|
long byteOff = lineText.left(vs.start).toUtf8().size();
|
||||||
|
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||||
|
(unsigned long)0, linePos + byteOff);
|
||||||
|
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||||
|
(unsigned long)0, linePos);
|
||||||
|
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||||
|
(unsigned long)h.line);
|
||||||
|
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
||||||
|
popup->showAt(anchor);
|
||||||
|
showPopup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
// Determine cursor shape based on interaction type
|
// Determine cursor shape based on interaction type
|
||||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||||
|
|
||||||
|
|||||||
10
src/editor.h
10
src/editor.h
@@ -54,6 +54,7 @@ public:
|
|||||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||||
QString textWithMargins() const;
|
QString textWithMargins() const;
|
||||||
void setCustomTypeNames(const QStringList& names);
|
void setCustomTypeNames(const QStringList& names);
|
||||||
|
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||||
|
|
||||||
// Saved sources for quick-switch in source picker
|
// Saved sources for quick-switch in source picker
|
||||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||||
@@ -78,7 +79,7 @@ private:
|
|||||||
LayoutInfo m_layout; // cached from ComposeResult
|
LayoutInfo m_layout; // cached from ComposeResult
|
||||||
|
|
||||||
// ── Toggle: absolute vs relative offset margin
|
// ── Toggle: absolute vs relative offset margin
|
||||||
bool m_relativeOffsets = false;
|
bool m_relativeOffsets = true;
|
||||||
|
|
||||||
int m_marginStyleBase = -1;
|
int m_marginStyleBase = -1;
|
||||||
int m_hintLine = -1;
|
int m_hintLine = -1;
|
||||||
@@ -129,7 +130,12 @@ private:
|
|||||||
// ── Saved sources for quick-switch ──
|
// ── Saved sources for quick-switch ──
|
||||||
QVector<SavedSourceDisplay> m_savedSourceDisplay;
|
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)
|
||||||
|
|
||||||
// ── Reentrancy guards ──
|
// ── Reentrancy guards ──
|
||||||
|
bool m_applyingDocument = false;
|
||||||
bool m_clampingSelection = false;
|
bool m_clampingSelection = false;
|
||||||
bool m_updatingComment = false;
|
bool m_updatingComment = false;
|
||||||
|
|
||||||
@@ -145,7 +151,7 @@ private:
|
|||||||
void applyMarkers(const QVector<LineMeta>& meta);
|
void applyMarkers(const QVector<LineMeta>& meta);
|
||||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||||
void applyCommandRowPills();
|
void applyCommandRowPills();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
204
src/export_reclass_xml.cpp
Normal file
204
src/export_reclass_xml.cpp
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#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]];
|
||||||
|
|
||||||
|
// 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
|
||||||
10
src/export_reclass_xml.h
Normal file
10
src/export_reclass_xml.h
Normal 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
|
||||||
Binary file not shown.
@@ -293,7 +293,6 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
|||||||
line += QStringLiteral("]");
|
line += QStringLiteral("]");
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
|
||||||
case NodeKind::UTF8: {
|
case NodeKind::UTF8: {
|
||||||
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
||||||
int end = bytes.indexOf('\0');
|
int end = bytes.indexOf('\0');
|
||||||
@@ -344,21 +343,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
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 (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);
|
const int sz = sizeForKind(node.kind);
|
||||||
QByteArray b = prov.isReadable(addr, sz)
|
QByteArray b = prov.isReadable(addr, sz)
|
||||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ static QString cTypeName(NodeKind kind) {
|
|||||||
case NodeKind::Mat4x4: return QStringLiteral("float");
|
case NodeKind::Mat4x4: return QStringLiteral("float");
|
||||||
case NodeKind::UTF8: return QStringLiteral("char");
|
case NodeKind::UTF8: return QStringLiteral("char");
|
||||||
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
||||||
case NodeKind::Padding: return QStringLiteral("uint8_t");
|
|
||||||
default: return QStringLiteral("uint8_t");
|
default: return QStringLiteral("uint8_t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +122,6 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
|||||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||||
case NodeKind::UTF16:
|
case NodeKind::UTF16:
|
||||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
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: {
|
case NodeKind::Pointer32: {
|
||||||
if (node.refId != 0) {
|
if (node.refId != 0) {
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
int refIdx = tree.indexOfId(node.refId);
|
||||||
@@ -169,7 +166,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
auto emitPadRun = [&](int offset, int size) {
|
auto emitPadRun = [&](int offset, int size) {
|
||||||
if (size <= 0) return;
|
if (size <= 0) return;
|
||||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
||||||
.arg(ctx.cType(NodeKind::Padding))
|
.arg(QStringLiteral("uint8_t"))
|
||||||
.arg(ctx.uniquePadName())
|
.arg(ctx.uniquePadName())
|
||||||
.arg(QString::number(size, 16).toUpper())
|
.arg(QString::number(size, 16).toUpper())
|
||||||
.arg(offsetComment(offset));
|
.arg(offsetComment(offset));
|
||||||
|
|||||||
388
src/import_reclass_xml.cpp
Normal file
388
src/import_reclass_xml.cpp
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
#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) {
|
||||||
|
if (ver == XmlVersion::V2016) {
|
||||||
|
for (const auto& e : kTypeMap2016)
|
||||||
|
if (e.xmlType == xmlType) return e.kind;
|
||||||
|
} else {
|
||||||
|
for (const auto& e : kTypeMap2013)
|
||||||
|
if (e.xmlType == xmlType) return e.kind;
|
||||||
|
}
|
||||||
|
return NodeKind::Hex8; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
tree.invalidateIdCache();
|
||||||
|
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
|
||||||
11
src/import_reclass_xml.h
Normal file
11
src/import_reclass_xml.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#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.
|
||||||
|
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||||
|
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
1066
src/import_source.cpp
Normal file
1066
src/import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
13
src/import_source.h
Normal file
13
src/import_source.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#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
|
||||||
|
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||||
|
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
708
src/main.cpp
708
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "titlebar.h"
|
||||||
#include "pluginmanager.h"
|
#include "pluginmanager.h"
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
#include <QMdiArea>
|
||||||
@@ -30,7 +31,7 @@ private slots:
|
|||||||
void openFile();
|
void openFile();
|
||||||
void saveFile();
|
void saveFile();
|
||||||
void saveFileAs();
|
void saveFileAs();
|
||||||
|
void closeFile();
|
||||||
|
|
||||||
void addNode();
|
void addNode();
|
||||||
void removeNode();
|
void removeNode();
|
||||||
@@ -46,8 +47,12 @@ private slots:
|
|||||||
void toggleMcp();
|
void toggleMcp();
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
void exportCpp();
|
void exportCpp();
|
||||||
|
void exportReclassXmlAction();
|
||||||
|
void importFromSource();
|
||||||
|
void importReclassXml();
|
||||||
void showTypeAliasesDialog();
|
void showTypeAliasesDialog();
|
||||||
void editTheme();
|
void editTheme();
|
||||||
|
void showOptionsDialog();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Project Lifecycle API
|
// Project Lifecycle API
|
||||||
@@ -61,6 +66,8 @@ private:
|
|||||||
|
|
||||||
QMdiArea* m_mdiArea;
|
QMdiArea* m_mdiArea;
|
||||||
QLabel* m_statusLabel;
|
QLabel* m_statusLabel;
|
||||||
|
TitleBarWidget* m_titleBar = nullptr;
|
||||||
|
QWidget* m_borderOverlay = nullptr;
|
||||||
PluginManager m_pluginManager;
|
PluginManager m_pluginManager;
|
||||||
McpBridge* m_mcp = nullptr;
|
McpBridge* m_mcp = nullptr;
|
||||||
QAction* m_mcpAction = nullptr;
|
QAction* m_mcpAction = nullptr;
|
||||||
@@ -104,6 +111,7 @@ private:
|
|||||||
SplitPane createSplitPane(TabState& tab);
|
SplitPane createSplitPane(TabState& tab);
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
void applyTabWidgetStyle(QTabWidget* tw);
|
void applyTabWidgetStyle(QTabWidget* tw);
|
||||||
|
void styleTabCloseButtons();
|
||||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||||
SplitPane* findActiveSplitPane();
|
SplitPane* findActiveSplitPane();
|
||||||
RcxEditor* activePaneEditor();
|
RcxEditor* activePaneEditor();
|
||||||
@@ -114,6 +122,11 @@ private:
|
|||||||
QStandardItemModel* m_workspaceModel = nullptr;
|
QStandardItemModel* m_workspaceModel = nullptr;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel();
|
void rebuildWorkspaceModel();
|
||||||
|
void updateBorderColor(const QColor& color);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void changeEvent(QEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
|
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
|
||||||
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
|
"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 "
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
@@ -793,7 +793,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.contains("pid")) {
|
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();
|
QString name = args.value("processName").toString();
|
||||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
||||||
QString target = QString("%1:%2").arg(pid).arg(name);
|
QString target = QString("%1:%2").arg(pid).arg(name);
|
||||||
|
|||||||
261
src/optionsdialog.cpp
Normal file
261
src/optionsdialog.cpp
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
#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);
|
||||||
|
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();
|
||||||
|
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
|
||||||
51
src/optionsdialog.h
Normal file
51
src/optionsdialog.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#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 = false;
|
||||||
|
int refreshMs = 660;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// searchable keywords per leaf tree item
|
||||||
|
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||||
|
// tree item → stacked widget page index
|
||||||
|
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
@@ -1,28 +1,65 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "provider.h"
|
#include "provider.h"
|
||||||
|
#include <QHash>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
// Provider that reads from a cached QByteArray snapshot but delegates
|
// Page-based snapshot provider.
|
||||||
// metadata (name, kind, getSymbol) to the underlying real provider.
|
//
|
||||||
// Used for async refresh: worker thread reads bulk data into a snapshot,
|
// During async refresh the controller reads pages for the main struct and
|
||||||
// UI thread composes against it without blocking.
|
// every reachable pointer target. Compose reads entirely from this page
|
||||||
|
// table — no fallback to the real provider, no blocking I/O on the UI
|
||||||
|
// thread. Pages that were never fetched (truly invalid pointers) simply
|
||||||
|
// read as zeros.
|
||||||
class SnapshotProvider : public Provider {
|
class SnapshotProvider : public Provider {
|
||||||
std::shared_ptr<Provider> m_real;
|
std::shared_ptr<Provider> m_real;
|
||||||
QByteArray m_data;
|
QHash<uint64_t, QByteArray> m_pages; // page-aligned addr → 4096-byte page
|
||||||
|
int m_mainExtent = 0; // logical size of the main struct range
|
||||||
|
|
||||||
|
static constexpr uint64_t kPageSize = 4096;
|
||||||
|
static constexpr uint64_t kPageMask = ~(kPageSize - 1);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SnapshotProvider(std::shared_ptr<Provider> real, QByteArray snapshot)
|
using PageMap = QHash<uint64_t, QByteArray>;
|
||||||
: m_real(std::move(real)), m_data(std::move(snapshot)) {}
|
|
||||||
|
SnapshotProvider(std::shared_ptr<Provider> real, PageMap pages, int mainExtent)
|
||||||
|
: m_real(std::move(real))
|
||||||
|
, m_pages(std::move(pages))
|
||||||
|
, m_mainExtent(mainExtent) {}
|
||||||
|
|
||||||
bool read(uint64_t addr, void* buf, int len) const override {
|
bool read(uint64_t addr, void* buf, int len) const override {
|
||||||
if (!isReadable(addr, len)) return false;
|
if (len <= 0) return false;
|
||||||
std::memcpy(buf, m_data.constData() + addr, len);
|
char* out = static_cast<char*>(buf);
|
||||||
|
uint64_t cur = addr;
|
||||||
|
int remaining = len;
|
||||||
|
while (remaining > 0) {
|
||||||
|
uint64_t pageAddr = cur & kPageMask;
|
||||||
|
int pageOff = static_cast<int>(cur - pageAddr);
|
||||||
|
int chunk = qMin(remaining, static_cast<int>(kPageSize - pageOff));
|
||||||
|
auto it = m_pages.constFind(pageAddr);
|
||||||
|
if (it != m_pages.constEnd()) {
|
||||||
|
std::memcpy(out, it->constData() + pageOff, chunk);
|
||||||
|
} else {
|
||||||
|
std::memset(out, 0, chunk);
|
||||||
|
}
|
||||||
|
out += chunk;
|
||||||
|
cur += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int size() const override { return m_data.size(); }
|
bool isReadable(uint64_t addr, int len) const override {
|
||||||
|
if (len <= 0) return (len == 0);
|
||||||
|
uint64_t end = addr + static_cast<uint64_t>(len);
|
||||||
|
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
|
||||||
|
if (!m_pages.contains(p)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() const override { return m_mainExtent; }
|
||||||
bool isWritable() const override { return m_real ? m_real->isWritable() : false; }
|
bool isWritable() const override { return m_real ? m_real->isWritable() : false; }
|
||||||
bool isLive() const override { return m_real ? m_real->isLive() : false; }
|
bool isLive() const override { return m_real ? m_real->isLive() : false; }
|
||||||
QString name() const override { return m_real ? m_real->name() : QString(); }
|
QString name() const override { return m_real ? m_real->name() : QString(); }
|
||||||
@@ -34,21 +71,36 @@ public:
|
|||||||
bool write(uint64_t addr, const void* buf, int len) override {
|
bool write(uint64_t addr, const void* buf, int len) override {
|
||||||
if (!m_real) return false;
|
if (!m_real) return false;
|
||||||
bool ok = m_real->write(addr, buf, len);
|
bool ok = m_real->write(addr, buf, len);
|
||||||
if (ok && isReadable(addr, len))
|
if (ok) patchPages(addr, buf, len);
|
||||||
std::memcpy(m_data.data() + addr, buf, len);
|
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the entire snapshot (called after async read completes)
|
// Replace the entire page table (called after async read completes)
|
||||||
void updateSnapshot(QByteArray data) { m_data = std::move(data); }
|
void updatePages(PageMap pages, int mainExtent) {
|
||||||
|
m_pages = std::move(pages);
|
||||||
// Patch specific bytes in the snapshot (called after user writes a value)
|
m_mainExtent = mainExtent;
|
||||||
void patchSnapshot(uint64_t addr, const void* buf, int len) {
|
|
||||||
if (isReadable(addr, len))
|
|
||||||
std::memcpy(m_data.data() + addr, buf, len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QByteArray& snapshot() const { return m_data; }
|
// Patch specific bytes in existing pages (called after user writes a value)
|
||||||
|
void patchPages(uint64_t addr, const void* buf, int len) {
|
||||||
|
const char* src = static_cast<const char*>(buf);
|
||||||
|
uint64_t cur = addr;
|
||||||
|
int remaining = len;
|
||||||
|
while (remaining > 0) {
|
||||||
|
uint64_t pageAddr = cur & kPageMask;
|
||||||
|
int pageOff = static_cast<int>(cur - pageAddr);
|
||||||
|
int chunk = qMin(remaining, static_cast<int>(kPageSize - pageOff));
|
||||||
|
auto it = m_pages.find(pageAddr);
|
||||||
|
if (it != m_pages.end()) {
|
||||||
|
std::memcpy(it->data() + pageOff, src, chunk);
|
||||||
|
}
|
||||||
|
src += chunk;
|
||||||
|
cur += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageMap& pages() const { return m_pages; }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<file alias="chevron-right.png">icons/chevron-right.png</file>
|
<file alias="chevron-right.png">icons/chevron-right.png</file>
|
||||||
<file alias="chevron-down.png">icons/chevron-down.png</file>
|
<file alias="chevron-down.png">icons/chevron-down.png</file>
|
||||||
<file alias="class.png">icons/class.png</file>
|
<file alias="class.png">icons/class.png</file>
|
||||||
|
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/fonts">
|
<qresource prefix="/fonts">
|
||||||
<file alias="JetBrainsMono.ttf">fonts/JetBrainsMono.ttf</file>
|
<file alias="JetBrainsMono.ttf">fonts/JetBrainsMono.ttf</file>
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
||||||
<file alias="split-horizontal.svg">vsicons/split-horizontal.svg</file>
|
<file alias="split-horizontal.svg">vsicons/split-horizontal.svg</file>
|
||||||
<file alias="chrome-close.svg">vsicons/chrome-close.svg</file>
|
<file alias="chrome-close.svg">vsicons/chrome-close.svg</file>
|
||||||
|
<file alias="chrome-minimize.svg">vsicons/chrome-minimize.svg</file>
|
||||||
|
<file alias="chrome-maximize.svg">vsicons/chrome-maximize.svg</file>
|
||||||
|
<file alias="chrome-restore.svg">vsicons/chrome-restore.svg</file>
|
||||||
<file alias="text-size.svg">vsicons/text-size.svg</file>
|
<file alias="text-size.svg">vsicons/text-size.svg</file>
|
||||||
<file alias="add.svg">vsicons/add.svg</file>
|
<file alias="add.svg">vsicons/add.svg</file>
|
||||||
<file alias="remove.svg">vsicons/remove.svg</file>
|
<file alias="remove.svg">vsicons/remove.svg</file>
|
||||||
@@ -43,5 +47,9 @@
|
|||||||
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
||||||
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
|
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
|
||||||
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.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>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
32
src/themes/defaults/reclass_dark.json
Normal file
32
src/themes/defaults/reclass_dark.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Reclass Dark",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"backgroundAlt": "#252526",
|
||||||
|
"surface": "#2a2d2e",
|
||||||
|
"border": "#3c3c3c",
|
||||||
|
"borderFocused": "#888888",
|
||||||
|
"button": "#333333",
|
||||||
|
"text": "#d4d4d4",
|
||||||
|
"textDim": "#858585",
|
||||||
|
"textMuted": "#585858",
|
||||||
|
"textFaint": "#505050",
|
||||||
|
"hover": "#1e1e1e",
|
||||||
|
"selected": "#1e1e1e",
|
||||||
|
"selection": "#2b2b2b",
|
||||||
|
"syntaxKeyword": "#569cd6",
|
||||||
|
"syntaxNumber": "#b5cea8",
|
||||||
|
"syntaxString": "#ce9178",
|
||||||
|
"syntaxComment": "#6a9955",
|
||||||
|
"syntaxPreproc": "#c586c0",
|
||||||
|
"syntaxType": "#4EC9B0",
|
||||||
|
"indHoverSpan": "#E6B450",
|
||||||
|
"indCmdPill": "#2a2a2a",
|
||||||
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHeatCold": "#D4A945",
|
||||||
|
"indHeatWarm": "#E6B450",
|
||||||
|
"indHeatHot": "#f44747",
|
||||||
|
"indHintGreen": "#5a8248",
|
||||||
|
"markerPtr": "#f44747",
|
||||||
|
"markerCycle": "#e5a00d",
|
||||||
|
"markerError": "#7a2e2e"
|
||||||
|
}
|
||||||
32
src/themes/defaults/vs.json
Normal file
32
src/themes/defaults/vs.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "VS2022 Dark",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"backgroundAlt": "#2d2d30",
|
||||||
|
"surface": "#333337",
|
||||||
|
"border": "#3f3f46",
|
||||||
|
"borderFocused": "#b180d7",
|
||||||
|
"button": "#3f3f46",
|
||||||
|
"text": "#dcdcdc",
|
||||||
|
"textDim": "#858585",
|
||||||
|
"textMuted": "#636369",
|
||||||
|
"textFaint": "#4d4d55",
|
||||||
|
"hover": "#2c2c2f",
|
||||||
|
"selected": "#262629",
|
||||||
|
"selection": "#264f78",
|
||||||
|
"syntaxKeyword": "#569cd6",
|
||||||
|
"syntaxNumber": "#b5cea8",
|
||||||
|
"syntaxString": "#d69d85",
|
||||||
|
"syntaxComment": "#57a64a",
|
||||||
|
"syntaxPreproc": "#9b9b9b",
|
||||||
|
"syntaxType": "#4ec9b0",
|
||||||
|
"indHoverSpan": "#b180d7",
|
||||||
|
"indCmdPill": "#2d2d30",
|
||||||
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHeatCold": "#D4A945",
|
||||||
|
"indHeatWarm": "#d69d85",
|
||||||
|
"indHeatHot": "#f44747",
|
||||||
|
"indHintGreen": "#5a8248",
|
||||||
|
"markerPtr": "#f44747",
|
||||||
|
"markerCycle": "#e5a00d",
|
||||||
|
"markerError": "#7a2e2e"
|
||||||
|
}
|
||||||
32
src/themes/defaults/warm.json
Normal file
32
src/themes/defaults/warm.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Warm",
|
||||||
|
"background": "#212121",
|
||||||
|
"backgroundAlt": "#2a2a2a",
|
||||||
|
"surface": "#2a2a2a",
|
||||||
|
"border": "#373737",
|
||||||
|
"borderFocused": "#888888",
|
||||||
|
"button": "#373737",
|
||||||
|
"text": "#AAA99F",
|
||||||
|
"textDim": "#7a7a6e",
|
||||||
|
"textMuted": "#555550",
|
||||||
|
"textFaint": "#464646",
|
||||||
|
"hover": "#282828",
|
||||||
|
"selected": "#262626",
|
||||||
|
"selection": "#21213A",
|
||||||
|
"syntaxKeyword": "#AA9565",
|
||||||
|
"syntaxNumber": "#AAA98C",
|
||||||
|
"syntaxString": "#6B3B21",
|
||||||
|
"syntaxComment": "#464646",
|
||||||
|
"syntaxPreproc": "#AA9565",
|
||||||
|
"syntaxType": "#6B959F",
|
||||||
|
"indHoverSpan": "#AA9565",
|
||||||
|
"indCmdPill": "#2a2a2a",
|
||||||
|
"indDataChanged": "#6B959F",
|
||||||
|
"indHeatCold": "#C4A44A",
|
||||||
|
"indHeatWarm": "#AA9565",
|
||||||
|
"indHeatHot": "#A05040",
|
||||||
|
"indHintGreen": "#464646",
|
||||||
|
"markerPtr": "#6B3B21",
|
||||||
|
"markerCycle": "#AA9565",
|
||||||
|
"markerError": "#3C2121"
|
||||||
|
}
|
||||||
@@ -1,118 +1,66 @@
|
|||||||
#include "theme.h"
|
#include "theme.h"
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
// ── Field table for DRY serialization ──
|
// ── Shared field metadata (serialization + editor UI) ──
|
||||||
|
|
||||||
struct ColorField { const char* key; QColor Theme::*ptr; };
|
const ThemeFieldMeta kThemeFields[] = {
|
||||||
|
{"background", "Background", "Chrome", &Theme::background},
|
||||||
static const ColorField kFields[] = {
|
{"backgroundAlt", "Background Alt", "Chrome", &Theme::backgroundAlt},
|
||||||
{"background", &Theme::background},
|
{"surface", "Surface", "Chrome", &Theme::surface},
|
||||||
{"backgroundAlt", &Theme::backgroundAlt},
|
{"border", "Border", "Chrome", &Theme::border},
|
||||||
{"surface", &Theme::surface},
|
{"borderFocused", "Border Focused", "Chrome", &Theme::borderFocused},
|
||||||
{"border", &Theme::border},
|
{"button", "Button", "Chrome", &Theme::button},
|
||||||
{"button", &Theme::button},
|
{"text", "Text", "Text", &Theme::text},
|
||||||
{"text", &Theme::text},
|
{"textDim", "Text Dim", "Text", &Theme::textDim},
|
||||||
{"textDim", &Theme::textDim},
|
{"textMuted", "Text Muted", "Text", &Theme::textMuted},
|
||||||
{"textMuted", &Theme::textMuted},
|
{"textFaint", "Text Faint", "Text", &Theme::textFaint},
|
||||||
{"textFaint", &Theme::textFaint},
|
{"hover", "Hover", "Interactive", &Theme::hover},
|
||||||
{"hover", &Theme::hover},
|
{"selected", "Selected", "Interactive", &Theme::selected},
|
||||||
{"selected", &Theme::selected},
|
{"selection", "Selection", "Interactive", &Theme::selection},
|
||||||
{"selection", &Theme::selection},
|
{"syntaxKeyword", "Keyword", "Syntax", &Theme::syntaxKeyword},
|
||||||
{"syntaxKeyword", &Theme::syntaxKeyword},
|
{"syntaxNumber", "Number", "Syntax", &Theme::syntaxNumber},
|
||||||
{"syntaxNumber", &Theme::syntaxNumber},
|
{"syntaxString", "String", "Syntax", &Theme::syntaxString},
|
||||||
{"syntaxString", &Theme::syntaxString},
|
{"syntaxComment", "Comment", "Syntax", &Theme::syntaxComment},
|
||||||
{"syntaxComment", &Theme::syntaxComment},
|
{"syntaxPreproc", "Preprocessor", "Syntax", &Theme::syntaxPreproc},
|
||||||
{"syntaxPreproc", &Theme::syntaxPreproc},
|
{"syntaxType", "Type", "Syntax", &Theme::syntaxType},
|
||||||
{"syntaxType", &Theme::syntaxType},
|
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||||
{"indHoverSpan", &Theme::indHoverSpan},
|
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||||
{"indCmdPill", &Theme::indCmdPill},
|
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||||
{"indDataChanged",&Theme::indDataChanged},
|
{"indHeatCold", "Heat Cold", "Indicators", &Theme::indHeatCold},
|
||||||
{"indHintGreen", &Theme::indHintGreen},
|
{"indHeatWarm", "Heat Warm", "Indicators", &Theme::indHeatWarm},
|
||||||
{"markerPtr", &Theme::markerPtr},
|
{"indHeatHot", "Heat Hot", "Indicators", &Theme::indHeatHot},
|
||||||
{"markerCycle", &Theme::markerCycle},
|
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||||
{"markerError", &Theme::markerError},
|
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||||
|
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||||
|
{"markerError", "Error", "Markers", &Theme::markerError},
|
||||||
};
|
};
|
||||||
|
const int kThemeFieldCount = static_cast<int>(std::extent_v<decltype(kThemeFields)>);
|
||||||
|
|
||||||
QJsonObject Theme::toJson() const {
|
QJsonObject Theme::toJson() const {
|
||||||
QJsonObject o;
|
QJsonObject o;
|
||||||
o["name"] = name;
|
o["name"] = name;
|
||||||
for (const auto& f : kFields)
|
for (int i = 0; i < kThemeFieldCount; i++)
|
||||||
o[f.key] = (this->*f.ptr).name();
|
o[kThemeFields[i].key] = (this->*kThemeFields[i].ptr).name();
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
Theme Theme::fromJson(const QJsonObject& o) {
|
Theme Theme::fromJson(const QJsonObject& o) {
|
||||||
Theme t = reclassDark();
|
|
||||||
t.name = o["name"].toString(t.name);
|
|
||||||
for (const auto& f : kFields) {
|
|
||||||
if (o.contains(f.key))
|
|
||||||
t.*f.ptr = QColor(o[f.key].toString());
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Built-in themes ──
|
|
||||||
|
|
||||||
Theme Theme::reclassDark() {
|
|
||||||
Theme t;
|
Theme t;
|
||||||
t.name = "Reclass Dark";
|
t.name = o["name"].toString("Untitled");
|
||||||
t.background = QColor("#1e1e1e");
|
for (int i = 0; i < kThemeFieldCount; i++) {
|
||||||
t.backgroundAlt = QColor("#252526");
|
if (o.contains(kThemeFields[i].key))
|
||||||
t.surface = QColor("#2a2d2e");
|
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||||
t.border = QColor("#3c3c3c");
|
|
||||||
t.button = QColor("#333333");
|
|
||||||
t.text = QColor("#d4d4d4");
|
|
||||||
t.textDim = QColor("#858585");
|
|
||||||
t.textMuted = QColor("#585858");
|
|
||||||
t.textFaint = QColor("#505050");
|
|
||||||
t.hover = QColor("#2b2b2b");
|
|
||||||
t.selected = QColor("#232323");
|
|
||||||
t.selection = QColor("#2b2b2b");
|
|
||||||
t.syntaxKeyword = QColor("#569cd6");
|
|
||||||
t.syntaxNumber = QColor("#b5cea8");
|
|
||||||
t.syntaxString = QColor("#ce9178");
|
|
||||||
t.syntaxComment = QColor("#6a9955");
|
|
||||||
t.syntaxPreproc = QColor("#c586c0");
|
|
||||||
t.syntaxType = QColor("#4EC9B0");
|
|
||||||
t.indHoverSpan = QColor("#E6B450");
|
|
||||||
t.indCmdPill = QColor("#2a2a2a");
|
|
||||||
t.indDataChanged= QColor("#8fbc7a");
|
|
||||||
t.indHintGreen = QColor("#5a8248");
|
|
||||||
t.markerPtr = QColor("#f44747");
|
|
||||||
t.markerCycle = QColor("#e5a00d");
|
|
||||||
t.markerError = QColor("#7a2e2e");
|
|
||||||
return t;
|
|
||||||
}
|
}
|
||||||
|
// Derive heat colors from the theme's own palette when keys are absent
|
||||||
Theme Theme::warm() {
|
// cold = muted yellow, warm = hover/string amber, hot = marker red
|
||||||
Theme t;
|
if (!t.indHeatCold.isValid())
|
||||||
t.name = "Warm";
|
t.indHeatCold = QColor("#D4A945");
|
||||||
t.background = QColor("#212121");
|
if (!t.indHeatWarm.isValid())
|
||||||
t.backgroundAlt = QColor("#2a2a2a");
|
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||||
t.surface = QColor("#2a2a2a");
|
if (!t.indHeatHot.isValid())
|
||||||
t.border = QColor("#373737");
|
t.indHeatHot = t.markerPtr;
|
||||||
t.button = QColor("#373737");
|
|
||||||
t.text = QColor("#AAA99F");
|
|
||||||
t.textDim = QColor("#7a7a6e");
|
|
||||||
t.textMuted = QColor("#555550");
|
|
||||||
t.textFaint = QColor("#464646");
|
|
||||||
t.hover = QColor("#373737");
|
|
||||||
t.selected = QColor("#2d2d2d");
|
|
||||||
t.selection = QColor("#21213A");
|
|
||||||
t.syntaxKeyword = QColor("#AA9565");
|
|
||||||
t.syntaxNumber = QColor("#AAA98C");
|
|
||||||
t.syntaxString = QColor("#6B3B21");
|
|
||||||
t.syntaxComment = QColor("#464646");
|
|
||||||
t.syntaxPreproc = QColor("#AA9565");
|
|
||||||
t.syntaxType = QColor("#6B959F");
|
|
||||||
t.indHoverSpan = QColor("#AA9565");
|
|
||||||
t.indCmdPill = QColor("#2a2a2a");
|
|
||||||
t.indDataChanged= QColor("#6B959F");
|
|
||||||
t.indHintGreen = QColor("#464646");
|
|
||||||
t.markerPtr = QColor("#6B3B21");
|
|
||||||
t.markerCycle = QColor("#AA9565");
|
|
||||||
t.markerError = QColor("#3C2121");
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct Theme {
|
|||||||
QColor backgroundAlt; // panels, tab selected, tooltips
|
QColor backgroundAlt; // panels, tab selected, tooltips
|
||||||
QColor surface; // alternateBase
|
QColor surface; // alternateBase
|
||||||
QColor border; // separators, menu borders
|
QColor border; // separators, menu borders
|
||||||
|
QColor borderFocused; // window border when focused
|
||||||
QColor button; // button bg
|
QColor button; // button bg
|
||||||
|
|
||||||
// ── Text ──
|
// ── Text ──
|
||||||
@@ -37,7 +38,10 @@ struct Theme {
|
|||||||
// ── Indicators ──
|
// ── Indicators ──
|
||||||
QColor indHoverSpan; // hover link text
|
QColor indHoverSpan; // hover link text
|
||||||
QColor indCmdPill; // command row pill bg
|
QColor indCmdPill; // command row pill bg
|
||||||
QColor indDataChanged; // changed data values
|
QColor indDataChanged; // changed data values (legacy, fallback for old themes)
|
||||||
|
QColor indHeatCold; // heatmap level 1 (changed once)
|
||||||
|
QColor indHeatWarm; // heatmap level 2 (moderate changes)
|
||||||
|
QColor indHeatHot; // heatmap level 3 (frequent changes)
|
||||||
QColor indHintGreen; // comment/hint text
|
QColor indHintGreen; // comment/hint text
|
||||||
|
|
||||||
// ── Markers ──
|
// ── Markers ──
|
||||||
@@ -47,9 +51,18 @@ struct Theme {
|
|||||||
|
|
||||||
QJsonObject toJson() const;
|
QJsonObject toJson() const;
|
||||||
static Theme fromJson(const QJsonObject& obj);
|
static Theme fromJson(const QJsonObject& obj);
|
||||||
|
|
||||||
static Theme reclassDark();
|
|
||||||
static Theme warm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Shared field metadata (serialization + editor UI) ──
|
||||||
|
|
||||||
|
struct ThemeFieldMeta {
|
||||||
|
const char* key; // JSON key
|
||||||
|
const char* label; // display label
|
||||||
|
const char* group; // section group name
|
||||||
|
QColor Theme::*ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const ThemeFieldMeta kThemeFields[];
|
||||||
|
extern const int kThemeFieldCount;
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
: QStringLiteral("File: %1").arg(path));
|
: QStringLiteral("File: %1").arg(path));
|
||||||
mainLayout->addWidget(m_fileInfoLabel);
|
mainLayout->addWidget(m_fileInfoLabel);
|
||||||
|
|
||||||
// ── Scrollable area for swatches + contrast ──
|
// ── Scrollable area for swatches ──
|
||||||
auto* scroll = new QScrollArea;
|
auto* scroll = new QScrollArea;
|
||||||
scroll->setWidgetResizable(true);
|
scroll->setWidgetResizable(true);
|
||||||
scroll->setFrameShape(QFrame::NoFrame);
|
scroll->setFrameShape(QFrame::NoFrame);
|
||||||
@@ -79,12 +80,17 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
|
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
|
||||||
scrollLayout->setSpacing(2);
|
scrollLayout->setSpacing(2);
|
||||||
|
|
||||||
// ── Color swatches ──
|
// ── Color swatches (driven by kThemeFields) ──
|
||||||
struct FieldDef { const char* label; QColor Theme::*ptr; };
|
const char* currentGroup = nullptr;
|
||||||
|
for (int fi = 0; fi < kThemeFieldCount; fi++) {
|
||||||
|
const auto& f = kThemeFields[fi];
|
||||||
|
|
||||||
|
// Section header on group change
|
||||||
|
if (!currentGroup || std::strcmp(currentGroup, f.group) != 0) {
|
||||||
|
scrollLayout->addWidget(makeSectionLabel(QString::fromLatin1(f.group)));
|
||||||
|
currentGroup = f.group;
|
||||||
|
}
|
||||||
|
|
||||||
auto addGroup = [&](const QString& title, std::initializer_list<FieldDef> fields) {
|
|
||||||
scrollLayout->addWidget(makeSectionLabel(title));
|
|
||||||
for (const auto& f : fields) {
|
|
||||||
int idx = m_swatches.size();
|
int idx = m_swatches.size();
|
||||||
|
|
||||||
auto* row = new QHBoxLayout;
|
auto* row = new QHBoxLayout;
|
||||||
@@ -117,45 +123,6 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
scrollLayout->addLayout(row);
|
scrollLayout->addLayout(row);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
addGroup("Chrome", {
|
|
||||||
{"Background", &Theme::background},
|
|
||||||
{"Background Alt", &Theme::backgroundAlt},
|
|
||||||
{"Surface", &Theme::surface},
|
|
||||||
{"Border", &Theme::border},
|
|
||||||
{"Button", &Theme::button},
|
|
||||||
});
|
|
||||||
addGroup("Text", {
|
|
||||||
{"Text", &Theme::text},
|
|
||||||
{"Text Dim", &Theme::textDim},
|
|
||||||
{"Text Muted", &Theme::textMuted},
|
|
||||||
{"Text Faint", &Theme::textFaint},
|
|
||||||
});
|
|
||||||
addGroup("Interactive", {
|
|
||||||
{"Hover", &Theme::hover},
|
|
||||||
{"Selected", &Theme::selected},
|
|
||||||
{"Selection", &Theme::selection},
|
|
||||||
});
|
|
||||||
addGroup("Syntax", {
|
|
||||||
{"Keyword", &Theme::syntaxKeyword},
|
|
||||||
{"Number", &Theme::syntaxNumber},
|
|
||||||
{"String", &Theme::syntaxString},
|
|
||||||
{"Comment", &Theme::syntaxComment},
|
|
||||||
{"Preprocessor", &Theme::syntaxPreproc},
|
|
||||||
{"Type", &Theme::syntaxType},
|
|
||||||
});
|
|
||||||
addGroup("Indicators", {
|
|
||||||
{"Hover Span", &Theme::indHoverSpan},
|
|
||||||
{"Cmd Pill", &Theme::indCmdPill},
|
|
||||||
{"Data Changed", &Theme::indDataChanged},
|
|
||||||
{"Hint Green", &Theme::indHintGreen},
|
|
||||||
});
|
|
||||||
addGroup("Markers", {
|
|
||||||
{"Pointer", &Theme::markerPtr},
|
|
||||||
{"Cycle", &Theme::markerCycle},
|
|
||||||
{"Error", &Theme::markerError},
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollLayout->addStretch();
|
scrollLayout->addStretch();
|
||||||
scroll->setWidget(scrollWidget);
|
scroll->setWidget(scrollWidget);
|
||||||
@@ -163,28 +130,21 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
// ── Bottom bar ──
|
// ── Bottom bar ──
|
||||||
auto* bottomRow = new QHBoxLayout;
|
auto* bottomRow = new QHBoxLayout;
|
||||||
m_previewBtn = new QPushButton(QStringLiteral("Live Preview"));
|
|
||||||
m_previewBtn->setCheckable(true);
|
|
||||||
connect(m_previewBtn, &QPushButton::toggled, this, [this](bool) { togglePreview(); });
|
|
||||||
bottomRow->addWidget(m_previewBtn);
|
|
||||||
|
|
||||||
bottomRow->addStretch();
|
bottomRow->addStretch();
|
||||||
|
|
||||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
|
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
|
||||||
if (m_previewing) {
|
|
||||||
ThemeManager::instance().revertPreview();
|
ThemeManager::instance().revertPreview();
|
||||||
m_previewing = false;
|
|
||||||
}
|
|
||||||
reject();
|
reject();
|
||||||
});
|
});
|
||||||
bottomRow->addWidget(buttons);
|
bottomRow->addWidget(buttons);
|
||||||
mainLayout->addLayout(bottomRow);
|
mainLayout->addLayout(bottomRow);
|
||||||
|
|
||||||
// Initial update
|
// Initial swatch update + start live preview
|
||||||
for (int i = 0; i < m_swatches.size(); i++)
|
for (int i = 0; i < m_swatches.size(); i++)
|
||||||
updateSwatch(i);
|
updateSwatch(i);
|
||||||
|
tm.previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load a different theme into the editor ──
|
// ── Load a different theme into the editor ──
|
||||||
@@ -206,7 +166,6 @@ void ThemeEditor::loadTheme(int index) {
|
|||||||
for (int i = 0; i < m_swatches.size(); i++)
|
for (int i = 0; i < m_swatches.size(); i++)
|
||||||
updateSwatch(i);
|
updateSwatch(i);
|
||||||
|
|
||||||
if (m_previewing)
|
|
||||||
tm.previewTheme(m_theme);
|
tm.previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,19 +189,8 @@ void ThemeEditor::pickColor(int idx) {
|
|||||||
if (c.isValid()) {
|
if (c.isValid()) {
|
||||||
m_theme.*s.field = c;
|
m_theme.*s.field = c;
|
||||||
updateSwatch(idx);
|
updateSwatch(idx);
|
||||||
if (m_previewing)
|
|
||||||
ThemeManager::instance().previewTheme(m_theme);
|
ThemeManager::instance().previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Live preview toggle ──
|
|
||||||
|
|
||||||
void ThemeEditor::togglePreview() {
|
|
||||||
m_previewing = m_previewBtn->isChecked();
|
|
||||||
if (m_previewing)
|
|
||||||
ThemeManager::instance().previewTheme(m_theme);
|
|
||||||
else
|
|
||||||
ThemeManager::instance().revertPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -36,14 +36,10 @@ private:
|
|||||||
QComboBox* m_themeCombo = nullptr;
|
QComboBox* m_themeCombo = nullptr;
|
||||||
QLineEdit* m_nameEdit = nullptr;
|
QLineEdit* m_nameEdit = nullptr;
|
||||||
QLabel* m_fileInfoLabel = nullptr;
|
QLabel* m_fileInfoLabel = nullptr;
|
||||||
QPushButton* m_previewBtn = nullptr;
|
|
||||||
bool m_previewing = false;
|
|
||||||
|
|
||||||
void loadTheme(int index);
|
void loadTheme(int index);
|
||||||
void rebuildSwatches(QVBoxLayout* swatchLayout);
|
|
||||||
void updateSwatch(int idx);
|
void updateSwatch(int idx);
|
||||||
void pickColor(int idx);
|
void pickColor(int idx);
|
||||||
void togglePreview();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -13,18 +14,40 @@ ThemeManager& ThemeManager::instance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeManager::ThemeManager() {
|
ThemeManager::ThemeManager() {
|
||||||
m_builtIn.append(Theme::reclassDark());
|
loadBuiltInThemes();
|
||||||
m_builtIn.append(Theme::warm());
|
|
||||||
loadUserThemes();
|
loadUserThemes();
|
||||||
|
|
||||||
QSettings settings("Reclass", "Reclass");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString saved = settings.value("theme", m_builtIn[0].name).toString();
|
QString fallback = m_builtIn.isEmpty() ? QString() : m_builtIn[0].name;
|
||||||
|
QString saved = settings.value("theme", fallback).toString();
|
||||||
auto all = themes();
|
auto all = themes();
|
||||||
for (int i = 0; i < all.size(); i++) {
|
for (int i = 0; i < all.size(); i++) {
|
||||||
if (all[i].name == saved) { m_currentIdx = i; break; }
|
if (all[i].name == saved) { m_currentIdx = i; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Load built-in themes from JSON files next to the executable ──
|
||||||
|
|
||||||
|
QString ThemeManager::builtInDir() const {
|
||||||
|
return QCoreApplication::applicationDirPath() + "/themes";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::loadBuiltInThemes() {
|
||||||
|
m_builtIn.clear();
|
||||||
|
QDir dir(builtInDir());
|
||||||
|
if (!dir.exists()) return;
|
||||||
|
for (const QString& name : dir.entryList({"*.json"}, QDir::Files, QDir::Name)) {
|
||||||
|
QFile f(dir.filePath(name));
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) continue;
|
||||||
|
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
if (jdoc.isObject())
|
||||||
|
m_builtIn.append(Theme::fromJson(jdoc.object()));
|
||||||
|
}
|
||||||
|
m_builtInDefaults = m_builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── themes / current ──
|
||||||
|
|
||||||
QVector<Theme> ThemeManager::themes() const {
|
QVector<Theme> ThemeManager::themes() const {
|
||||||
QVector<Theme> all = m_builtIn;
|
QVector<Theme> all = m_builtIn;
|
||||||
all.append(m_user);
|
all.append(m_user);
|
||||||
@@ -37,7 +60,10 @@ const Theme& ThemeManager::current() const {
|
|||||||
int userIdx = m_currentIdx - m_builtIn.size();
|
int userIdx = m_currentIdx - m_builtIn.size();
|
||||||
if (userIdx >= 0 && userIdx < m_user.size())
|
if (userIdx >= 0 && userIdx < m_user.size())
|
||||||
return m_user[userIdx];
|
return m_user[userIdx];
|
||||||
|
if (!m_builtIn.isEmpty())
|
||||||
return m_builtIn[0];
|
return m_builtIn[0];
|
||||||
|
static const Theme empty;
|
||||||
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::setCurrent(int index) {
|
void ThemeManager::setCurrent(int index) {
|
||||||
@@ -55,16 +81,19 @@ void ThemeManager::addTheme(const Theme& theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
||||||
|
m_previewing = false; // commit any active preview
|
||||||
|
|
||||||
if (index < builtInCount()) {
|
if (index < builtInCount()) {
|
||||||
// Can't overwrite built-in; save as user theme instead
|
m_builtIn[index] = theme;
|
||||||
m_user.append(theme);
|
m_currentIdx = index;
|
||||||
} else {
|
} else {
|
||||||
int ui = index - builtInCount();
|
int ui = index - builtInCount();
|
||||||
if (ui >= 0 && ui < m_user.size())
|
if (ui >= 0 && ui < m_user.size())
|
||||||
m_user[ui] = theme;
|
m_user[ui] = theme;
|
||||||
}
|
}
|
||||||
saveUserThemes();
|
saveUserThemes();
|
||||||
if (index == m_currentIdx)
|
QSettings settings("Reclass", "Reclass");
|
||||||
|
settings.setValue("theme", current().name);
|
||||||
emit themeChanged(current());
|
emit themeChanged(current());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +111,9 @@ void ThemeManager::removeTheme(int index) {
|
|||||||
saveUserThemes();
|
saveUserThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ThemeManager::themesDir() const {
|
// ── User theme persistence ──
|
||||||
|
|
||||||
|
QString ThemeManager::userDir() const {
|
||||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||||
+ "/themes";
|
+ "/themes";
|
||||||
QDir().mkpath(dir);
|
QDir().mkpath(dir);
|
||||||
@@ -91,37 +122,69 @@ QString ThemeManager::themesDir() const {
|
|||||||
|
|
||||||
void ThemeManager::loadUserThemes() {
|
void ThemeManager::loadUserThemes() {
|
||||||
m_user.clear();
|
m_user.clear();
|
||||||
QDir dir(themesDir());
|
QDir dir(userDir());
|
||||||
for (const QString& name : dir.entryList({"*.json"}, QDir::Files)) {
|
for (const QString& name : dir.entryList({"*.json"}, QDir::Files)) {
|
||||||
QFile f(dir.filePath(name));
|
QFile f(dir.filePath(name));
|
||||||
if (!f.open(QIODevice::ReadOnly)) continue;
|
if (!f.open(QIODevice::ReadOnly)) continue;
|
||||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||||
if (jdoc.isObject())
|
if (!jdoc.isObject()) continue;
|
||||||
m_user.append(Theme::fromJson(jdoc.object()));
|
Theme t = Theme::fromJson(jdoc.object());
|
||||||
|
|
||||||
|
// If this overrides a built-in (same name), replace it in-place
|
||||||
|
bool isOverride = false;
|
||||||
|
for (int i = 0; i < m_builtIn.size(); i++) {
|
||||||
|
if (m_builtIn[i].name == t.name) {
|
||||||
|
m_builtIn[i] = t;
|
||||||
|
isOverride = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isOverride)
|
||||||
|
m_user.append(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::saveUserThemes() const {
|
void ThemeManager::saveUserThemes() const {
|
||||||
QString dir = themesDir();
|
QString dir = userDir();
|
||||||
// Remove old files
|
|
||||||
QDir d(dir);
|
QDir d(dir);
|
||||||
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
||||||
d.remove(name);
|
d.remove(name);
|
||||||
// Write current user themes
|
|
||||||
|
// Save modified built-ins (compare against on-disk originals)
|
||||||
|
for (int i = 0; i < m_builtIn.size() && i < m_builtInDefaults.size(); i++) {
|
||||||
|
if (m_builtIn[i].toJson() != m_builtInDefaults[i].toJson()) {
|
||||||
|
QString filename = m_builtIn[i].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
QFile f(dir + "/" + filename);
|
||||||
|
if (f.open(QIODevice::WriteOnly))
|
||||||
|
f.write(QJsonDocument(m_builtIn[i].toJson()).toJson(QJsonDocument::Indented));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user themes
|
||||||
for (int i = 0; i < m_user.size(); i++) {
|
for (int i = 0; i < m_user.size(); i++) {
|
||||||
QString filename = m_user[i].name.toLower().replace(' ', '_') + ".json";
|
QString filename = m_user[i].name.toLower().replace(' ', '_') + ".json";
|
||||||
QFile f(dir + "/" + filename);
|
QFile f(dir + "/" + filename);
|
||||||
if (!f.open(QIODevice::WriteOnly)) continue;
|
if (f.open(QIODevice::WriteOnly))
|
||||||
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ThemeManager::themeFilePath(int index) const {
|
QString ThemeManager::themeFilePath(int index) const {
|
||||||
if (index < builtInCount()) return {};
|
if (index < builtInCount()) {
|
||||||
|
// Built-in has a user override file only if modified
|
||||||
|
if (index < m_builtInDefaults.size()
|
||||||
|
&& m_builtIn[index].toJson() != m_builtInDefaults[index].toJson()) {
|
||||||
|
QString filename = m_builtIn[index].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
return userDir() + "/" + filename;
|
||||||
|
}
|
||||||
|
// Show the built-in source file
|
||||||
|
QString filename = m_builtIn[index].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
return builtInDir() + "/" + filename;
|
||||||
|
}
|
||||||
int ui = index - builtInCount();
|
int ui = index - builtInCount();
|
||||||
if (ui < 0 || ui >= m_user.size()) return {};
|
if (ui < 0 || ui >= m_user.size()) return {};
|
||||||
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
|
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
|
||||||
return themesDir() + "/" + filename;
|
return userDir() + "/" + filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::previewTheme(const Theme& theme) {
|
void ThemeManager::previewTheme(const Theme& theme) {
|
||||||
|
|||||||
@@ -31,14 +31,17 @@ signals:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
ThemeManager();
|
ThemeManager();
|
||||||
QVector<Theme> m_builtIn;
|
QVector<Theme> m_builtIn; // built-in themes (possibly overridden)
|
||||||
|
QVector<Theme> m_builtInDefaults; // originals loaded from disk
|
||||||
QVector<Theme> m_user;
|
QVector<Theme> m_user;
|
||||||
int m_currentIdx = 0;
|
int m_currentIdx = 0;
|
||||||
|
|
||||||
int builtInCount() const { return m_builtIn.size(); }
|
int builtInCount() const { return m_builtIn.size(); }
|
||||||
QString themesDir() const;
|
void loadBuiltInThemes();
|
||||||
|
QString builtInDir() const;
|
||||||
|
QString userDir() const;
|
||||||
bool m_previewing = false;
|
bool m_previewing = false;
|
||||||
Theme m_savedTheme; // stashed current theme during preview
|
Theme m_savedTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
186
src/titlebar.cpp
Normal file
186
src/titlebar.cpp
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#include "titlebar.h"
|
||||||
|
#include "themes/thememanager.h"
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
TitleBarWidget::TitleBarWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_theme(ThemeManager::instance().current())
|
||||||
|
{
|
||||||
|
setFixedHeight(32);
|
||||||
|
|
||||||
|
auto* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
// App name
|
||||||
|
m_appLabel = new QLabel(QStringLiteral("Reclass"), this);
|
||||||
|
m_appLabel->setContentsMargins(10, 0, 4, 0);
|
||||||
|
m_appLabel->setAlignment(Qt::AlignVCenter);
|
||||||
|
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
layout->addWidget(m_appLabel);
|
||||||
|
|
||||||
|
// Menu bar
|
||||||
|
m_menuBar = new QMenuBar(this);
|
||||||
|
m_menuBar->setNativeMenuBar(false);
|
||||||
|
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||||
|
layout->addWidget(m_menuBar);
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
// Chrome buttons
|
||||||
|
m_btnMin = makeChromeButton(":/vsicons/chrome-minimize.svg");
|
||||||
|
m_btnMax = makeChromeButton(":/vsicons/chrome-maximize.svg");
|
||||||
|
m_btnClose = makeChromeButton(":/vsicons/chrome-close.svg");
|
||||||
|
|
||||||
|
layout->addWidget(m_btnMin);
|
||||||
|
layout->addWidget(m_btnMax);
|
||||||
|
layout->addWidget(m_btnClose);
|
||||||
|
|
||||||
|
connect(m_btnMin, &QToolButton::clicked, this, [this]() {
|
||||||
|
window()->showMinimized();
|
||||||
|
});
|
||||||
|
connect(m_btnMax, &QToolButton::clicked, this, [this]() {
|
||||||
|
toggleMaximize();
|
||||||
|
});
|
||||||
|
connect(m_btnClose, &QToolButton::clicked, this, [this]() {
|
||||||
|
window()->close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QToolButton* TitleBarWidget::makeChromeButton(const QString& iconPath) {
|
||||||
|
auto* btn = new QToolButton(this);
|
||||||
|
btn->setIcon(QIcon(iconPath));
|
||||||
|
btn->setIconSize(QSize(16, 16));
|
||||||
|
btn->setFixedSize(46, 32);
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
btn->setFocusPolicy(Qt::NoFocus);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||||
|
m_theme = theme;
|
||||||
|
|
||||||
|
// Title bar background
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
QPalette pal = palette();
|
||||||
|
pal.setColor(QPalette::Window, theme.background);
|
||||||
|
setPalette(pal);
|
||||||
|
|
||||||
|
// App label
|
||||||
|
m_appLabel->setStyleSheet(
|
||||||
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
|
.arg(theme.textDim.name()));
|
||||||
|
|
||||||
|
// Menu bar styling — transparent background, themed text
|
||||||
|
m_menuBar->setStyleSheet(
|
||||||
|
QStringLiteral(
|
||||||
|
"QMenuBar { background: transparent; border: none; }"
|
||||||
|
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
|
||||||
|
"QMenuBar::item:selected { background: %2; }"
|
||||||
|
"QMenuBar::item:pressed { background: %2; }")
|
||||||
|
.arg(theme.textDim.name(), theme.hover.name()));
|
||||||
|
|
||||||
|
// Chrome buttons
|
||||||
|
QString btnStyle = QStringLiteral(
|
||||||
|
"QToolButton { background: transparent; border: none; }"
|
||||||
|
"QToolButton:hover { background: %1; }")
|
||||||
|
.arg(theme.hover.name());
|
||||||
|
m_btnMin->setStyleSheet(btnStyle);
|
||||||
|
m_btnMax->setStyleSheet(btnStyle);
|
||||||
|
|
||||||
|
// Close button: red hover
|
||||||
|
m_btnClose->setStyleSheet(QStringLiteral(
|
||||||
|
"QToolButton { background: transparent; border: none; }"
|
||||||
|
"QToolButton:hover { background: #c42b1c; }"));
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::setShowIcon(bool show) {
|
||||||
|
if (show) {
|
||||||
|
m_appLabel->setText(QString());
|
||||||
|
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
|
||||||
|
} else {
|
||||||
|
m_appLabel->setPixmap(QPixmap());
|
||||||
|
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||||
|
m_appLabel->setStyleSheet(
|
||||||
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
|
.arg(m_theme.textDim.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
||||||
|
m_titleCase = titleCase;
|
||||||
|
for (QAction* action : m_menuBar->actions()) {
|
||||||
|
QString text = action->text();
|
||||||
|
QString clean = text;
|
||||||
|
clean.remove('&');
|
||||||
|
|
||||||
|
if (titleCase) {
|
||||||
|
action->setText("&" + clean.toUpper());
|
||||||
|
} else {
|
||||||
|
QString result;
|
||||||
|
bool capitalizeNext = true;
|
||||||
|
for (int i = 0; i < clean.length(); ++i) {
|
||||||
|
QChar ch = clean[i];
|
||||||
|
if (ch.isLetter()) {
|
||||||
|
result += capitalizeNext ? ch.toUpper() : ch.toLower();
|
||||||
|
capitalizeNext = false;
|
||||||
|
} else {
|
||||||
|
result += ch;
|
||||||
|
if (ch.isSpace()) capitalizeNext = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action->setText("&" + result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::updateMaximizeIcon() {
|
||||||
|
if (window()->isMaximized())
|
||||||
|
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));
|
||||||
|
else
|
||||||
|
m_btnMax->setIcon(QIcon(":/vsicons/chrome-maximize.svg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::toggleMaximize() {
|
||||||
|
if (window()->isMaximized())
|
||||||
|
window()->showNormal();
|
||||||
|
else
|
||||||
|
window()->showMaximized();
|
||||||
|
updateMaximizeIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::mousePressEvent(QMouseEvent* event) {
|
||||||
|
if (event->button() == Qt::LeftButton) {
|
||||||
|
window()->windowHandle()->startSystemMove();
|
||||||
|
event->accept();
|
||||||
|
} else {
|
||||||
|
QWidget::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::mouseDoubleClickEvent(QMouseEvent* event) {
|
||||||
|
if (event->button() == Qt::LeftButton) {
|
||||||
|
toggleMaximize();
|
||||||
|
event->accept();
|
||||||
|
} else {
|
||||||
|
QWidget::mouseDoubleClickEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleBarWidget::paintEvent(QPaintEvent* event) {
|
||||||
|
QWidget::paintEvent(event);
|
||||||
|
|
||||||
|
// 1px bottom border
|
||||||
|
QPainter p(this);
|
||||||
|
p.setPen(m_theme.border);
|
||||||
|
p.drawLine(0, height() - 1, width() - 1, height() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
43
src/titlebar.h
Normal file
43
src/titlebar.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "themes/theme.h"
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QMenuBar>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
class TitleBarWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TitleBarWidget(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
QMenuBar* menuBar() const { return m_menuBar; }
|
||||||
|
void applyTheme(const Theme& theme);
|
||||||
|
void setShowIcon(bool show);
|
||||||
|
void setMenuBarTitleCase(bool titleCase);
|
||||||
|
bool menuBarTitleCase() const { return m_titleCase; }
|
||||||
|
|
||||||
|
void updateMaximizeIcon();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_appLabel = nullptr;
|
||||||
|
QMenuBar* m_menuBar = nullptr;
|
||||||
|
QToolButton* m_btnMin = nullptr;
|
||||||
|
QToolButton* m_btnMax = nullptr;
|
||||||
|
QToolButton* m_btnClose = nullptr;
|
||||||
|
|
||||||
|
Theme m_theme;
|
||||||
|
bool m_titleCase = true;
|
||||||
|
|
||||||
|
QToolButton* makeChromeButton(const QString& iconPath);
|
||||||
|
void toggleMaximize();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QIntValidator>
|
#include <QIntValidator>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -121,7 +122,7 @@ public:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 18px gutter: side triangle if current
|
// Gutter: side triangle if current
|
||||||
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
||||||
const TypeEntry& entry = (*m_filtered)[row];
|
const TypeEntry& entry = (*m_filtered)[row];
|
||||||
bool isCurrent = false;
|
bool isCurrent = false;
|
||||||
@@ -130,13 +131,13 @@ public:
|
|||||||
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||||
isCurrent = (entry.structId == m_current->structId);
|
isCurrent = (entry.structId == m_current->structId);
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
painter->setPen(t.syntaxType);
|
painter->setPen(t.text);
|
||||||
painter->setFont(m_font);
|
painter->setFont(m_font);
|
||||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
painter->drawText(QRect(x, y, 10, h), Qt::AlignCenter,
|
||||||
QString(QChar(0x25B8)));
|
QString(QChar(0x25B8)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x += 18;
|
x += 10;
|
||||||
|
|
||||||
// Icon 16x16 — only for composite entries
|
// Icon 16x16 — only for composite entries
|
||||||
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
@@ -335,6 +336,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
m_arrayCountEdit->setVisible(id == 3);
|
m_arrayCountEdit->setVisible(id == 3);
|
||||||
if (id == 3) m_arrayCountEdit->setFocus();
|
if (id == 3) m_arrayCountEdit->setFocus();
|
||||||
updateModifierPreview();
|
updateModifierPreview();
|
||||||
|
applyFilter(m_filterEdit->text());
|
||||||
});
|
});
|
||||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||||
this, [this]() { updateModifierPreview(); });
|
this, [this]() { updateModifierPreview(); });
|
||||||
@@ -368,6 +370,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
m_listView->setFrameShape(QFrame::NoFrame);
|
m_listView->setFrameShape(QFrame::NoFrame);
|
||||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
m_listView->setMouseTracking(true);
|
m_listView->setMouseTracking(true);
|
||||||
|
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||||
m_listView->installEventFilter(this);
|
m_listView->installEventFilter(this);
|
||||||
|
|
||||||
@@ -384,10 +387,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::warmUp() {
|
void TypeSelectorPopup::warmUp() {
|
||||||
|
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
|
||||||
|
// subsystem the first time a popup with complex children is shown. Pre-pay it
|
||||||
|
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
|
||||||
|
// ourselves.
|
||||||
|
{
|
||||||
|
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||||
|
primer->resize(300, 400);
|
||||||
|
auto* lay = new QVBoxLayout(primer);
|
||||||
|
lay->addWidget(new QLabel(QStringLiteral("x")));
|
||||||
|
lay->addWidget(new QLineEdit);
|
||||||
|
auto* model = new QStringListModel(primer);
|
||||||
|
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
|
||||||
|
model->setStringList(items);
|
||||||
|
auto* lv = new QListView;
|
||||||
|
lv->setModel(model);
|
||||||
|
lay->addWidget(lv);
|
||||||
|
primer->show();
|
||||||
|
QApplication::processEvents();
|
||||||
|
primer->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
delete primer;
|
||||||
|
}
|
||||||
|
|
||||||
TypeEntry dummy;
|
TypeEntry dummy;
|
||||||
dummy.entryKind = TypeEntry::Primitive;
|
dummy.entryKind = TypeEntry::Primitive;
|
||||||
dummy.primitiveKind = NodeKind::Hex8;
|
dummy.primitiveKind = NodeKind::Hex8;
|
||||||
dummy.displayName = "warmup";
|
dummy.displayName = QStringLiteral("warmup");
|
||||||
setTypes({dummy});
|
setTypes({dummy});
|
||||||
popup(QPoint(-9999, -9999));
|
popup(QPoint(-9999, -9999));
|
||||||
hide();
|
hide();
|
||||||
@@ -467,7 +493,7 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
|||||||
QString text = t.classKeyword.isEmpty()
|
QString text = t.classKeyword.isEmpty()
|
||||||
? t.displayName
|
? t.displayName
|
||||||
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16;
|
int w = 10 + 20 + fm.horizontalAdvance(text) + 16;
|
||||||
if (w > maxTextW) maxTextW = w;
|
if (w > maxTextW) maxTextW = w;
|
||||||
}
|
}
|
||||||
int popupW = qBound(280, maxTextW + 24, 500);
|
int popupW = qBound(280, maxTextW + 24, 500);
|
||||||
@@ -537,6 +563,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
|||||||
|
|
||||||
QString filterBase = text.trimmed();
|
QString filterBase = text.trimmed();
|
||||||
|
|
||||||
|
// Hide primitives when a pointer modifier (* or **) is active
|
||||||
|
int modId = m_modGroup->checkedId();
|
||||||
|
bool hideprimitives = (modId == 1 || modId == 2);
|
||||||
|
|
||||||
// Separate primitives and composites
|
// Separate primitives and composites
|
||||||
QVector<TypeEntry> primitives, composites;
|
QVector<TypeEntry> primitives, composites;
|
||||||
for (const auto& t : m_allTypes) {
|
for (const auto& t : m_allTypes) {
|
||||||
@@ -546,9 +576,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
|||||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||||
if (!matchesFilter) continue;
|
if (!matchesFilter) continue;
|
||||||
|
|
||||||
if (t.entryKind == TypeEntry::Primitive)
|
if (t.entryKind == TypeEntry::Primitive) {
|
||||||
|
if (!hideprimitives)
|
||||||
primitives.append(t);
|
primitives.append(t);
|
||||||
else if (t.entryKind == TypeEntry::Composite)
|
} else if (t.entryKind == TypeEntry::Composite)
|
||||||
composites.append(t);
|
composites.append(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,76 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include <QIcon>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
// Recursively add children of parentId as tree items under parentItem.
|
struct TabInfo {
|
||||||
inline void addWorkspaceChildren(QStandardItem* parentItem,
|
const NodeTree* tree;
|
||||||
const NodeTree& tree,
|
QString name;
|
||||||
uint64_t parentId,
|
void* subPtr; // QMdiSubWindow* as void*
|
||||||
void* subPtr) {
|
};
|
||||||
QVector<int> children = tree.childrenOf(parentId);
|
|
||||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (int idx : children) {
|
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||||
const Node& node = tree.nodes[idx];
|
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||||
|
|
||||||
// Skip hex preview nodes — they are padding/filler, not meaningful fields
|
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||||
if (isHexNode(node.kind)) continue;
|
const QVector<TabInfo>& tabs) {
|
||||||
|
|
||||||
QString display;
|
|
||||||
if (node.kind == NodeKind::Struct) {
|
|
||||||
QString typeName = node.structTypeName.isEmpty()
|
|
||||||
? node.name : node.structTypeName;
|
|
||||||
display = QStringLiteral("%1 (%2)")
|
|
||||||
.arg(typeName, node.resolvedClassKeyword());
|
|
||||||
} else {
|
|
||||||
display = QStringLiteral("%1 (%2)")
|
|
||||||
.arg(node.name, QString::fromLatin1(kindToString(node.kind)));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* item = new QStandardItem(display);
|
|
||||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
|
||||||
if (node.kind == NodeKind::Struct)
|
|
||||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1);
|
|
||||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll
|
|
||||||
|
|
||||||
if (node.kind == NodeKind::Struct)
|
|
||||||
addWorkspaceChildren(item, tree, node.id, subPtr);
|
|
||||||
|
|
||||||
parentItem->appendRow(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void buildWorkspaceModel(QStandardItemModel* model,
|
|
||||||
const NodeTree& tree,
|
|
||||||
const QString& projectName,
|
|
||||||
void* subPtr = nullptr) {
|
|
||||||
model->clear();
|
model->clear();
|
||||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||||
|
|
||||||
auto* projectItem = new QStandardItem(projectName);
|
// Single "Project" root with folder icon
|
||||||
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr;
|
||||||
|
auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"),
|
||||||
|
QStringLiteral("Project"));
|
||||||
|
projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole);
|
||||||
|
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||||
|
|
||||||
addWorkspaceChildren(projectItem, tree, 0, subPtr);
|
// Collect all top-level structs/enums across all tabs
|
||||||
|
QVector<std::pair<const Node*, void*>> types, enums;
|
||||||
|
for (const auto& tab : tabs) {
|
||||||
|
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||||
|
for (int idx : topLevel) {
|
||||||
|
const Node& n = tab.tree->nodes[idx];
|
||||||
|
if (n.kind != NodeKind::Struct) continue;
|
||||||
|
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||||
|
enums.append({&n, tab.subPtr});
|
||||||
|
else
|
||||||
|
types.append({&n, tab.subPtr});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nameOf = [](const Node* n) {
|
||||||
|
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||||
|
};
|
||||||
|
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
||||||
|
const std::pair<const Node*, void*>& b) {
|
||||||
|
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
||||||
|
};
|
||||||
|
std::sort(types.begin(), types.end(), cmpName);
|
||||||
|
std::sort(enums.begin(), enums.end(), cmpName);
|
||||||
|
|
||||||
|
for (const auto& [n, subPtr] : types) {
|
||||||
|
QString display = QStringLiteral("%1 (%2)")
|
||||||
|
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||||
|
auto* item = new QStandardItem(
|
||||||
|
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||||
|
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||||
|
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||||
|
projectItem->appendRow(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [n, subPtr] : enums) {
|
||||||
|
QString display = QStringLiteral("%1 (%2)")
|
||||||
|
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||||
|
auto* item = new QStandardItem(
|
||||||
|
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||||
|
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||||
|
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||||
|
projectItem->appendRow(item);
|
||||||
|
}
|
||||||
|
|
||||||
model->appendRow(projectItem);
|
model->appendRow(projectItem);
|
||||||
}
|
}
|
||||||
|
|||||||
185
tests/test_com_security.cpp
Normal file
185
tests/test_com_security.cpp
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* test_com_security.cpp — DebugConnect transport diagnostic
|
||||||
|
*
|
||||||
|
* Tests EVERY transport to find what works from MinGW:
|
||||||
|
* 1. TCP to WinDbg .server (port 5055)
|
||||||
|
* 2. Named pipe to WinDbg .server
|
||||||
|
* 3. TCP with various COM security configs
|
||||||
|
* 4. DebugCreate local (baseline)
|
||||||
|
*
|
||||||
|
* SETUP: In WinDbg, run BOTH of these:
|
||||||
|
* .server tcp:port=5055
|
||||||
|
* .server npipe:pipe=reclass
|
||||||
|
*
|
||||||
|
* Then run this test.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <objbase.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <dbgeng.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
static void try_connect(const char* label, const char* connStr)
|
||||||
|
{
|
||||||
|
printf(" %-40s → ", label);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
IDebugClient* client = nullptr;
|
||||||
|
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr) && client) {
|
||||||
|
printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr);
|
||||||
|
|
||||||
|
// Try to get data spaces and read something
|
||||||
|
IDebugDataSpaces* ds = nullptr;
|
||||||
|
IDebugSymbols* sym = nullptr;
|
||||||
|
IDebugControl* ctrl = nullptr;
|
||||||
|
client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||||
|
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||||
|
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||||
|
|
||||||
|
if (ctrl) {
|
||||||
|
HRESULT hrWait = ctrl->WaitForEvent(0, 5000);
|
||||||
|
printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sym) {
|
||||||
|
ULONG numMods = 0, numUnloaded = 0;
|
||||||
|
sym->GetNumberModules(&numMods, &numUnloaded);
|
||||||
|
printf(" Modules: %lu loaded\n", numMods);
|
||||||
|
|
||||||
|
if (numMods > 0 && ds) {
|
||||||
|
ULONG64 base = 0;
|
||||||
|
sym->GetModuleByIndex(0, &base);
|
||||||
|
unsigned char buf[2] = {};
|
||||||
|
ULONG got = 0;
|
||||||
|
ds->ReadVirtual(base, buf, 2, &got);
|
||||||
|
printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n",
|
||||||
|
(unsigned long long)base, got, buf[0], buf[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sym) sym->Release();
|
||||||
|
if (ds) ds->Release();
|
||||||
|
if (ctrl) ctrl->Release();
|
||||||
|
client->Release();
|
||||||
|
} else {
|
||||||
|
char buf[256] = {};
|
||||||
|
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr);
|
||||||
|
for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p)
|
||||||
|
*p = '\0';
|
||||||
|
printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
char hostname[256] = {};
|
||||||
|
DWORD hsize = sizeof(hostname);
|
||||||
|
GetComputerNameA(hostname, &hsize);
|
||||||
|
|
||||||
|
printf("=== DebugConnect Transport Diagnostic ===\n");
|
||||||
|
printf("Machine: %s\n\n", hostname);
|
||||||
|
|
||||||
|
// ── Baseline: DebugCreate (local) ──
|
||||||
|
printf("[1] DebugCreate (local, no network)\n");
|
||||||
|
{
|
||||||
|
IDebugClient* client = nullptr;
|
||||||
|
HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client);
|
||||||
|
printf(" DebugCreate: %s (hr=0x%08lX)\n\n",
|
||||||
|
SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr);
|
||||||
|
if (client) client->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TCP variants ──
|
||||||
|
printf("[2] TCP connections (need: .server tcp:port=5055)\n");
|
||||||
|
try_connect("tcp:Port=5055,Server=localhost",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
try_connect("tcp:Port=5055,Server=127.0.0.1",
|
||||||
|
"tcp:Port=5055,Server=127.0.0.1");
|
||||||
|
{
|
||||||
|
char conn[512];
|
||||||
|
snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname);
|
||||||
|
try_connect(conn, conn);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
// ── Named pipe variants ──
|
||||||
|
printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n");
|
||||||
|
try_connect("npipe:Pipe=reclass,Server=localhost",
|
||||||
|
"npipe:Pipe=reclass,Server=localhost");
|
||||||
|
{
|
||||||
|
char conn[512];
|
||||||
|
snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname);
|
||||||
|
try_connect(conn, conn);
|
||||||
|
}
|
||||||
|
try_connect("npipe:Pipe=reclass",
|
||||||
|
"npipe:Pipe=reclass");
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
// ── TCP with COM security ──
|
||||||
|
printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n");
|
||||||
|
{
|
||||||
|
// This runs in-process so CoInitialize affects subsequent calls
|
||||||
|
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
|
CoInitializeSecurity(
|
||||||
|
nullptr, -1, nullptr, nullptr,
|
||||||
|
RPC_C_AUTHN_LEVEL_DEFAULT,
|
||||||
|
RPC_C_IMP_LEVEL_IMPERSONATE,
|
||||||
|
nullptr, EOAC_NONE, nullptr);
|
||||||
|
try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
try_connect("npipe:Pipe=reclass (MTA+SEC)",
|
||||||
|
"npipe:Pipe=reclass,Server=localhost");
|
||||||
|
CoUninitialize();
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
// ── Check if dbgeng.dll is the system one ──
|
||||||
|
printf("[5] DbgEng DLL info\n");
|
||||||
|
{
|
||||||
|
HMODULE hmod = GetModuleHandleA("dbgeng.dll");
|
||||||
|
if (hmod) {
|
||||||
|
char path[MAX_PATH] = {};
|
||||||
|
GetModuleFileNameA(hmod, path, MAX_PATH);
|
||||||
|
printf(" dbgeng.dll loaded from: %s\n", path);
|
||||||
|
|
||||||
|
// Get version
|
||||||
|
DWORD verSize = GetFileVersionInfoSizeA(path, nullptr);
|
||||||
|
if (verSize > 0) {
|
||||||
|
auto* verData = (char*)malloc(verSize);
|
||||||
|
if (GetFileVersionInfoA(path, 0, verSize, verData)) {
|
||||||
|
VS_FIXEDFILEINFO* fileInfo = nullptr;
|
||||||
|
UINT len = 0;
|
||||||
|
if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) {
|
||||||
|
printf(" Version: %d.%d.%d.%d\n",
|
||||||
|
HIWORD(fileInfo->dwFileVersionMS),
|
||||||
|
LOWORD(fileInfo->dwFileVersionMS),
|
||||||
|
HIWORD(fileInfo->dwFileVersionLS),
|
||||||
|
LOWORD(fileInfo->dwFileVersionLS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(verData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf(" dbgeng.dll not loaded yet\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n=== Done ===\n");
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
printf("Windows only.\n");
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ private slots:
|
|||||||
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
|
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testPaddingMarker() {
|
void testHexNodeCompose() {
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
@@ -100,19 +100,18 @@ private slots:
|
|||||||
int ri = tree.addNode(root);
|
int ri = tree.addNode(root);
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
Node pad;
|
Node hex;
|
||||||
pad.kind = NodeKind::Padding;
|
hex.kind = NodeKind::Hex8;
|
||||||
pad.name = "pad";
|
hex.name = "pad";
|
||||||
pad.parentId = rootId;
|
hex.parentId = rootId;
|
||||||
pad.offset = 0;
|
hex.offset = 0;
|
||||||
tree.addNode(pad);
|
tree.addNode(hex);
|
||||||
|
|
||||||
NullProvider prov;
|
NullProvider prov;
|
||||||
ComposeResult result = compose(tree, prov);
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
// CommandRow + padding + root footer = 3
|
// CommandRow + hex node + root footer = 3
|
||||||
QCOMPARE(result.meta.size(), 3);
|
QCOMPARE(result.meta.size(), 3);
|
||||||
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
|
|
||||||
QCOMPARE(result.meta[1].depth, 1);
|
QCOMPARE(result.meta[1].depth, 1);
|
||||||
|
|
||||||
// Line 2 is root footer
|
// Line 2 is root footer
|
||||||
|
|||||||
@@ -8,6 +8,26 @@
|
|||||||
|
|
||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
|
|
||||||
|
// Provider with a configurable base address (for testing source-switch logic)
|
||||||
|
class BaseAwareProvider : public Provider {
|
||||||
|
QByteArray m_data;
|
||||||
|
uint64_t m_base;
|
||||||
|
public:
|
||||||
|
BaseAwareProvider(QByteArray data, uint64_t base)
|
||||||
|
: m_data(std::move(data)), m_base(base) {}
|
||||||
|
bool read(uint64_t addr, void* buf, int len) const override {
|
||||||
|
if (addr + len > (uint64_t)m_data.size()) return false;
|
||||||
|
std::memcpy(buf, m_data.constData() + addr, len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int size() const override { return m_data.size(); }
|
||||||
|
uint64_t base() const override { return m_base; }
|
||||||
|
void setBase(uint64_t b) override { m_base = b; }
|
||||||
|
bool isLive() const override { return true; }
|
||||||
|
QString name() const override { return QStringLiteral("test"); }
|
||||||
|
QString kind() const override { return QStringLiteral("Process"); }
|
||||||
|
};
|
||||||
|
|
||||||
// Small tree: one root struct with a few typed fields at known offsets.
|
// Small tree: one root struct with a few typed fields at known offsets.
|
||||||
// Keeps tests fast and deterministic (no giant PEB tree).
|
// Keeps tests fast and deterministic (no giant PEB tree).
|
||||||
static void buildSmallTree(NodeTree& tree) {
|
static void buildSmallTree(NodeTree& tree) {
|
||||||
@@ -34,9 +54,8 @@ static void buildSmallTree(NodeTree& tree) {
|
|||||||
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
||||||
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
||||||
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
||||||
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding
|
field(9, NodeKind::Hex16, "pad0"); // 2 bytes
|
||||||
// Set padding arrayLen = 3 for 3-byte padding
|
field(11, NodeKind::Hex8, "pad1"); // 1 byte
|
||||||
tree.nodes.last().arrayLen = 3;
|
|
||||||
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,47 +301,6 @@ private slots:
|
|||||||
QVERIFY(newIdx >= 0);
|
QVERIFY(newIdx >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: Padding value edit is effectively blocked at controller level ──
|
|
||||||
void testPaddingValueEditIsBlocked() {
|
|
||||||
// Find the padding node
|
|
||||||
int padIdx = -1;
|
|
||||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
|
||||||
if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; }
|
|
||||||
}
|
|
||||||
QVERIFY(padIdx >= 0);
|
|
||||||
uint64_t addr = m_doc->tree.computeOffset(padIdx);
|
|
||||||
|
|
||||||
// Read original data at padding offset
|
|
||||||
int padSize = m_doc->tree.nodes[padIdx].byteSize();
|
|
||||||
QByteArray origData = m_doc->provider->readBytes(addr, padSize);
|
|
||||||
|
|
||||||
// The context menu blocks Padding editing, so the controller's setNodeValue
|
|
||||||
// would only be called if the editing UI somehow allows it. But let's verify
|
|
||||||
// the editor correctly blocks it.
|
|
||||||
// Find padding line in composed output
|
|
||||||
ComposeResult result = m_doc->compose();
|
|
||||||
int paddingLine = -1;
|
|
||||||
for (int i = 0; i < result.meta.size(); i++) {
|
|
||||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
|
||||||
result.meta[i].lineKind == LineKind::Field) {
|
|
||||||
paddingLine = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY(paddingLine >= 0);
|
|
||||||
|
|
||||||
m_editor->applyDocument(result);
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// beginInlineEdit(Value) on Padding line must be rejected
|
|
||||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
|
||||||
QVERIFY(!m_editor->isEditing());
|
|
||||||
|
|
||||||
// Data must be unchanged
|
|
||||||
QByteArray afterData = m_doc->provider->readBytes(addr, padSize);
|
|
||||||
QCOMPARE(afterData, origData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
||||||
void testSetNodeValueHex() {
|
void testSetNodeValueHex() {
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
@@ -425,6 +403,48 @@ private slots:
|
|||||||
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
|
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: source switch preserves existing base address ──
|
||||||
|
void testSourceSwitchPreservesBase() {
|
||||||
|
// Document already has baseAddress = 0x1000 from buildSmallTree()
|
||||||
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||||
|
|
||||||
|
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
|
||||||
|
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x400000);
|
||||||
|
uint64_t newBase = prov->base();
|
||||||
|
QCOMPARE(newBase, (uint64_t)0x400000);
|
||||||
|
|
||||||
|
m_doc->provider = prov;
|
||||||
|
// This is the controller logic under test:
|
||||||
|
if (m_doc->tree.baseAddress == 0)
|
||||||
|
m_doc->tree.baseAddress = newBase;
|
||||||
|
else
|
||||||
|
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||||
|
|
||||||
|
// baseAddress must stay at the original value
|
||||||
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||||
|
// provider base must be synced to match
|
||||||
|
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: source switch on fresh doc uses provider default ──
|
||||||
|
void testSourceSwitchFreshDocUsesProviderBase() {
|
||||||
|
// Simulate a fresh document (no loaded .rcx → baseAddress == 0)
|
||||||
|
m_doc->tree.baseAddress = 0;
|
||||||
|
|
||||||
|
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x7FFE0000);
|
||||||
|
uint64_t newBase = prov->base();
|
||||||
|
|
||||||
|
m_doc->provider = prov;
|
||||||
|
if (m_doc->tree.baseAddress == 0)
|
||||||
|
m_doc->tree.baseAddress = newBase;
|
||||||
|
else
|
||||||
|
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||||
|
|
||||||
|
// Fresh doc should adopt the provider's default base
|
||||||
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
|
||||||
|
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Test: toggleCollapse + undo ──
|
// ── Test: toggleCollapse + undo ──
|
||||||
void testToggleCollapse() {
|
void testToggleCollapse() {
|
||||||
// Root is index 0, a Struct node
|
// Root is index 0, a Struct node
|
||||||
@@ -448,6 +468,181 @@ private slots:
|
|||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||||
}
|
}
|
||||||
|
// ── Test: value history popup only appears during inline editing ──
|
||||||
|
void testValueHistoryPopupOnlyDuringEdit() {
|
||||||
|
// Record value history for field_u32 so it has heat
|
||||||
|
auto& tree = m_doc->tree;
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
uint64_t nodeId = tree.nodes[idx].id;
|
||||||
|
|
||||||
|
QHash<uint64_t, ValueHistory> history;
|
||||||
|
history[nodeId].record("100");
|
||||||
|
history[nodeId].record("200");
|
||||||
|
history[nodeId].record("300");
|
||||||
|
QVERIFY(history[nodeId].uniqueCount() > 1);
|
||||||
|
|
||||||
|
m_editor->setValueHistoryRef(&history);
|
||||||
|
|
||||||
|
// Refresh and compose so editor has meta with heatLevel
|
||||||
|
m_ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
ComposeResult result = m_doc->compose();
|
||||||
|
// Manually set heat on the node's line meta
|
||||||
|
for (auto& lm : result.meta) {
|
||||||
|
if (lm.nodeId == nodeId) lm.heatLevel = 2;
|
||||||
|
}
|
||||||
|
m_editor->applyDocument(result);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Popup should not exist or not be visible (no editing active)
|
||||||
|
auto* popup = m_editor->findChild<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
|
||||||
|
// Even if popup widget exists, it should not be visible
|
||||||
|
bool popupVisible = false;
|
||||||
|
for (auto* child : m_editor->findChildren<QFrame*>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||||
|
if (child->isVisible() && child->windowFlags() & Qt::ToolTip)
|
||||||
|
popupVisible = true;
|
||||||
|
}
|
||||||
|
QVERIFY2(!popupVisible, "Popup should not be visible when not editing");
|
||||||
|
|
||||||
|
// Start inline edit on value column of field_u32
|
||||||
|
int fieldLine = -1;
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
if (result.meta[i].nodeId == nodeId && result.meta[i].lineKind == LineKind::Field) {
|
||||||
|
fieldLine = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(fieldLine >= 0);
|
||||||
|
|
||||||
|
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
|
||||||
|
QVERIFY(ok);
|
||||||
|
QVERIFY(m_editor->isEditing());
|
||||||
|
|
||||||
|
// Trigger hover cursor update (simulates mouse move during editing)
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Cancel edit to clean up
|
||||||
|
m_editor->cancelInlineEdit();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
m_editor->setValueHistoryRef(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: delete node clears value history for shifted siblings ──
|
||||||
|
void testDeleteClearsHeatForShiftedNodes() {
|
||||||
|
// Replace with a live provider so refresh() actually records values
|
||||||
|
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0x1000);
|
||||||
|
m_ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto& tree = m_doc->tree;
|
||||||
|
|
||||||
|
// Locate field_u32 (the node we'll delete) and the siblings after it.
|
||||||
|
// The small tree has: field_u32(0), field_float(4), field_u8(8),
|
||||||
|
// pad0/Hex16(9), pad1/Hex8(11), field_hex/Hex32(12)
|
||||||
|
// field_float and field_u8 are regular (non-hex) types.
|
||||||
|
int delIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == "field_u32") { delIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(delIdx >= 0);
|
||||||
|
uint64_t delId = tree.nodes[delIdx].id;
|
||||||
|
|
||||||
|
// Collect sibling node IDs that come after field_u32 (will be shifted)
|
||||||
|
uint64_t parentId = tree.nodes[delIdx].parentId;
|
||||||
|
int deletedSize = tree.nodes[delIdx].byteSize(); // 4 bytes
|
||||||
|
int deletedEnd = tree.nodes[delIdx].offset + deletedSize;
|
||||||
|
QVector<uint64_t> shiftedIds;
|
||||||
|
QHash<uint64_t, QString> nameMap; // for debug messages
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].parentId == parentId && i != delIdx
|
||||||
|
&& tree.nodes[i].offset >= deletedEnd) {
|
||||||
|
shiftedIds.append(tree.nodes[i].id);
|
||||||
|
nameMap[tree.nodes[i].id] = tree.nodes[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(!shiftedIds.isEmpty(), "Should have siblings after field_u32");
|
||||||
|
|
||||||
|
// Seed value history for shifted siblings (simulate accumulated heat)
|
||||||
|
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||||
|
for (uint64_t id : shiftedIds) {
|
||||||
|
history[id].record("old_val_1");
|
||||||
|
history[id].record("old_val_2");
|
||||||
|
history[id].record("old_val_3");
|
||||||
|
QVERIFY2(history[id].heatLevel() >= 2,
|
||||||
|
qPrintable(QString("Pre-delete: %1 should have heat>=2")
|
||||||
|
.arg(nameMap[id])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also seed the to-be-deleted node
|
||||||
|
history[delId].record("del_1");
|
||||||
|
history[delId].record("del_2");
|
||||||
|
QVERIFY(history.contains(delId));
|
||||||
|
|
||||||
|
// Delete field_u32 — this shifts all subsequent siblings
|
||||||
|
m_ctrl->removeNode(delIdx);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// The deleted node's history should be gone
|
||||||
|
QVERIFY2(!m_ctrl->valueHistory().contains(delId),
|
||||||
|
"Deleted node's value history should be cleared");
|
||||||
|
|
||||||
|
// All shifted siblings should have heat=0 after the delete.
|
||||||
|
// With a live provider, refresh() inside removeNode re-records one new
|
||||||
|
// value at the new offset → count=1 → heatLevel=0.
|
||||||
|
for (uint64_t id : shiftedIds) {
|
||||||
|
int heat = m_ctrl->valueHistory().contains(id)
|
||||||
|
? m_ctrl->valueHistory()[id].heatLevel() : 0;
|
||||||
|
QVERIFY2(heat == 0,
|
||||||
|
qPrintable(QString("Shifted node '%1' (id=%2) should have heat=0, got %3")
|
||||||
|
.arg(nameMap[id]).arg(id).arg(heat)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: value history records and cycles correctly ──
|
||||||
|
void testValueHistoryRingBuffer() {
|
||||||
|
ValueHistory vh;
|
||||||
|
QCOMPARE(vh.count, 0);
|
||||||
|
QCOMPARE(vh.heatLevel(), 0);
|
||||||
|
|
||||||
|
vh.record("10");
|
||||||
|
QCOMPARE(vh.count, 1);
|
||||||
|
QCOMPARE(vh.heatLevel(), 0); // 1 unique = static
|
||||||
|
|
||||||
|
// Duplicate should not increase count
|
||||||
|
vh.record("10");
|
||||||
|
QCOMPARE(vh.count, 1);
|
||||||
|
|
||||||
|
vh.record("20");
|
||||||
|
QCOMPARE(vh.count, 2);
|
||||||
|
QCOMPARE(vh.heatLevel(), 1); // cold
|
||||||
|
|
||||||
|
vh.record("30");
|
||||||
|
QCOMPARE(vh.count, 3);
|
||||||
|
QCOMPARE(vh.heatLevel(), 2); // warm
|
||||||
|
|
||||||
|
vh.record("40");
|
||||||
|
vh.record("50");
|
||||||
|
QCOMPARE(vh.count, 5);
|
||||||
|
QCOMPARE(vh.heatLevel(), 3); // hot
|
||||||
|
|
||||||
|
QCOMPARE(vh.last(), QString("50"));
|
||||||
|
|
||||||
|
// Ring buffer: uniqueCount() caps at kCapacity
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
vh.record(QString::number(100 + i));
|
||||||
|
QCOMPARE(vh.uniqueCount(), ValueHistory::kCapacity);
|
||||||
|
QVERIFY(vh.count > ValueHistory::kCapacity);
|
||||||
|
|
||||||
|
// forEach iterates oldest→newest within ring
|
||||||
|
QStringList vals;
|
||||||
|
vh.forEach([&](const QString& v) { vals.append(v); });
|
||||||
|
QCOMPARE(vals.size(), ValueHistory::kCapacity);
|
||||||
|
QCOMPARE(vals.last(), vh.last());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestController)
|
QTEST_MAIN(TestController)
|
||||||
|
|||||||
@@ -583,6 +583,94 @@ private slots:
|
|||||||
QCOMPARE(norm.size(), 1);
|
QCOMPARE(norm.size(), 1);
|
||||||
QVERIFY(norm.contains(rootId));
|
QVERIFY(norm.contains(rootId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ValueHistory tests ──
|
||||||
|
|
||||||
|
void testValueHistory_empty() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
QCOMPARE(h.heatLevel(), 0);
|
||||||
|
QCOMPARE(h.uniqueCount(), 0);
|
||||||
|
QCOMPARE(h.last(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_singleValue() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("42");
|
||||||
|
QCOMPARE(h.heatLevel(), 0); // only 1 unique → static
|
||||||
|
QCOMPARE(h.uniqueCount(), 1);
|
||||||
|
QCOMPARE(h.last(), QString("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_duplicateIgnored() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("42");
|
||||||
|
h.record("42");
|
||||||
|
h.record("42");
|
||||||
|
QCOMPARE(h.count, 1);
|
||||||
|
QCOMPARE(h.heatLevel(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_heatLevels() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("a");
|
||||||
|
QCOMPARE(h.heatLevel(), 0); // 1 unique
|
||||||
|
|
||||||
|
h.record("b");
|
||||||
|
QCOMPARE(h.heatLevel(), 1); // 2 unique → cold
|
||||||
|
|
||||||
|
h.record("c");
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // 3 unique → warm
|
||||||
|
|
||||||
|
h.record("d");
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // 4 unique → warm
|
||||||
|
|
||||||
|
h.record("e");
|
||||||
|
QCOMPARE(h.heatLevel(), 3); // 5 unique → hot
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_ringWrap() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
// Fill beyond capacity
|
||||||
|
for (int i = 0; i < 15; i++)
|
||||||
|
h.record(QString::number(i));
|
||||||
|
|
||||||
|
QCOMPARE(h.count, 15);
|
||||||
|
QCOMPARE(h.uniqueCount(), 10); // capped at kCapacity
|
||||||
|
QCOMPARE(h.heatLevel(), 3); // hot
|
||||||
|
QCOMPARE(h.last(), QString("14"));
|
||||||
|
|
||||||
|
// Verify oldest values were pushed out, newest 10 remain
|
||||||
|
QStringList collected;
|
||||||
|
h.forEach([&](const QString& v) { collected.append(v); });
|
||||||
|
QCOMPARE(collected.size(), 10);
|
||||||
|
QCOMPARE(collected.first(), QString("5")); // oldest surviving
|
||||||
|
QCOMPARE(collected.last(), QString("14")); // newest
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_forEach() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("x");
|
||||||
|
h.record("y");
|
||||||
|
h.record("z");
|
||||||
|
|
||||||
|
QStringList items;
|
||||||
|
h.forEach([&](const QString& v) { items.append(v); });
|
||||||
|
QCOMPARE(items.size(), 3);
|
||||||
|
QCOMPARE(items[0], QString("x"));
|
||||||
|
QCOMPARE(items[1], QString("y"));
|
||||||
|
QCOMPARE(items[2], QString("z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_oscillation() {
|
||||||
|
// Values that oscillate (A → B → A → B) should still count each unique transition
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("A");
|
||||||
|
h.record("B");
|
||||||
|
h.record("A");
|
||||||
|
h.record("B");
|
||||||
|
QCOMPARE(h.count, 4); // 4 transitions
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestCore)
|
QTEST_MAIN(TestCore)
|
||||||
|
|||||||
65
tests/test_dbgconnect.cpp
Normal file
65
tests/test_dbgconnect.cpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <dbgeng.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const char* connStr = "tcp:Port=5057,Server=localhost";
|
||||||
|
printf("Attempting DebugConnect(\"%s\")...\n", connStr);
|
||||||
|
|
||||||
|
IDebugClient* client = nullptr;
|
||||||
|
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||||
|
printf("DebugConnect returned: 0x%08lX\n", hr);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr) && client) {
|
||||||
|
printf("Connected! Getting IDebugDataSpaces...\n");
|
||||||
|
|
||||||
|
IDebugDataSpaces* ds = nullptr;
|
||||||
|
hr = client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||||
|
printf("QueryInterface(IDebugDataSpaces) = 0x%08lX\n", hr);
|
||||||
|
|
||||||
|
if (ds) {
|
||||||
|
IDebugControl* ctrl = nullptr;
|
||||||
|
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||||
|
|
||||||
|
if (ctrl) {
|
||||||
|
printf("Waiting for event...\n");
|
||||||
|
hr = ctrl->WaitForEvent(0, 5000);
|
||||||
|
printf("WaitForEvent = 0x%08lX\n", hr);
|
||||||
|
ctrl->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read 2 bytes
|
||||||
|
IDebugSymbols* sym = nullptr;
|
||||||
|
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||||
|
if (sym) {
|
||||||
|
ULONG numMods = 0, numUnloaded = 0;
|
||||||
|
hr = sym->GetNumberModules(&numMods, &numUnloaded);
|
||||||
|
printf("GetNumberModules = 0x%08lX, numMods=%lu\n", hr, numMods);
|
||||||
|
|
||||||
|
if (numMods > 0) {
|
||||||
|
ULONG64 base = 0;
|
||||||
|
hr = sym->GetModuleByIndex(0, &base);
|
||||||
|
printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr) && base) {
|
||||||
|
uint8_t buf[4] = {};
|
||||||
|
ULONG got = 0;
|
||||||
|
hr = ds->ReadVirtual(base, buf, 4, &got);
|
||||||
|
printf("ReadVirtual(%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n",
|
||||||
|
base, hr, got, buf[0], buf[1], buf[2], buf[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sym->Release();
|
||||||
|
}
|
||||||
|
ds->Release();
|
||||||
|
}
|
||||||
|
client->Release();
|
||||||
|
} else {
|
||||||
|
printf("DebugConnect FAILED. hr=0x%08lX\n", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -170,9 +170,10 @@ static NodeTree makeTestTree() {
|
|||||||
n.parentId = rootId; n.offset = off;
|
n.parentId = rootId; n.offset = off;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
};
|
};
|
||||||
auto pad = [&](int off, int len, const char* name) {
|
auto pad = [&](int off, int /*len*/, const char* name) {
|
||||||
Node n; n.kind = NodeKind::Padding; n.name = name;
|
// 4-byte padding → Hex32 (all usages in this test pass len=4)
|
||||||
n.parentId = rootId; n.offset = off; n.arrayLen = len;
|
Node n; n.kind = NodeKind::Hex32; n.name = name;
|
||||||
|
n.parentId = rootId; n.offset = off;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
};
|
};
|
||||||
auto arr = [&](int off, NodeKind ek, int len, const char* name) {
|
auto arr = [&](int off, NodeKind ek, int len, const char* name) {
|
||||||
@@ -278,8 +279,8 @@ static NodeTree makeTestTree() {
|
|||||||
|
|
||||||
n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n);
|
n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n);
|
||||||
n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n);
|
n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n);
|
||||||
n.kind = NodeKind::Padding; n.name = "Pad";
|
n.kind = NodeKind::Hex32; n.name = "Pad";
|
||||||
n.offset = 4; n.arrayLen = 4; tree.addNode(n);
|
n.offset = 4; n.arrayLen = 1; tree.addNode(n);
|
||||||
n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1;
|
n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
}
|
}
|
||||||
@@ -751,70 +752,6 @@ private slots:
|
|||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: Padding line rejects value editing ──
|
|
||||||
void testPaddingLineRejectsValueEdit() {
|
|
||||||
m_editor->applyDocument(m_result);
|
|
||||||
|
|
||||||
// Find a Padding line in the composed output
|
|
||||||
int paddingLine = -1;
|
|
||||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
|
||||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
|
||||||
m_result.meta[i].lineKind == LineKind::Field) {
|
|
||||||
paddingLine = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY2(paddingLine >= 0, "Should have at least one Padding line in test tree");
|
|
||||||
|
|
||||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
|
||||||
QVERIFY(lm);
|
|
||||||
QCOMPARE(lm->nodeKind, NodeKind::Padding);
|
|
||||||
|
|
||||||
// Value edit on Padding MUST be rejected (the bug fix)
|
|
||||||
QVERIFY2(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine),
|
|
||||||
"Value edit should be rejected on Padding lines");
|
|
||||||
QVERIFY(!m_editor->isEditing());
|
|
||||||
|
|
||||||
// Name edit on Padding SHOULD succeed (ASCII preview column is editable)
|
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Name, paddingLine);
|
|
||||||
QVERIFY2(ok, "Name edit should be allowed on Padding lines (ASCII preview)");
|
|
||||||
QVERIFY(m_editor->isEditing());
|
|
||||||
m_editor->cancelInlineEdit();
|
|
||||||
|
|
||||||
// Type edit on Padding SHOULD succeed (emits popup signal)
|
|
||||||
QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested);
|
|
||||||
ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine);
|
|
||||||
QVERIFY2(ok, "Type edit should be allowed on Padding lines");
|
|
||||||
QCOMPARE(typeSpy.count(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ──
|
|
||||||
void testPaddingLineRejectsValueSpan() {
|
|
||||||
m_editor->applyDocument(m_result);
|
|
||||||
|
|
||||||
// Find a Padding line
|
|
||||||
int paddingLine = -1;
|
|
||||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
|
||||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
|
||||||
m_result.meta[i].lineKind == LineKind::Field) {
|
|
||||||
paddingLine = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY(paddingLine >= 0);
|
|
||||||
|
|
||||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
|
||||||
QVERIFY(lm);
|
|
||||||
|
|
||||||
// valueSpanFor returns valid (shared with Hex via KF_HexPreview)
|
|
||||||
ColumnSpan vs = RcxEditor::valueSpan(*lm, 200);
|
|
||||||
QVERIFY2(vs.valid, "valueSpanFor should return valid for Padding (shared HexPreview flag)");
|
|
||||||
|
|
||||||
// But beginInlineEdit should still reject it
|
|
||||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
|
||||||
QVERIFY(!m_editor->isEditing());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Test: value edit commit fires signal with typed text ──
|
// ── Test: value edit commit fires signal with typed text ──
|
||||||
void testValueEditCommitUpdatesSignal() {
|
void testValueEditCommitUpdatesSignal() {
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
@@ -823,8 +760,6 @@ private slots:
|
|||||||
const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
|
const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
|
||||||
QVERIFY(lm);
|
QVERIFY(lm);
|
||||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||||
QVERIFY(lm->nodeKind != NodeKind::Padding);
|
|
||||||
|
|
||||||
// Begin value edit
|
// Begin value edit
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
|
bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
|
||||||
QVERIFY(ok);
|
QVERIFY(ok);
|
||||||
@@ -1064,6 +999,144 @@ private slots:
|
|||||||
"Root header should be suppressed from compose output");
|
"Root header should be suppressed from compose output");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: command row hover indicator survives refresh cycle ──
|
||||||
|
void testCommandRowHoverSurvivesRefresh() {
|
||||||
|
// IND_HOVER_SPAN = 11 (defined in editor.cpp, replicate for test)
|
||||||
|
constexpr int IND_HOVER_SPAN = 11;
|
||||||
|
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
|
// Set command row text (simulates controller.updateCommandRow)
|
||||||
|
QString cmdText = QStringLiteral(
|
||||||
|
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
|
||||||
|
m_editor->setCommandRowText(cmdText);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Parse the source span on line 0
|
||||||
|
auto* sci = m_editor->scintilla();
|
||||||
|
int len = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||||
|
QVERIFY(len > 0);
|
||||||
|
QByteArray buf(len + 1, '\0');
|
||||||
|
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
|
||||||
|
(void*)buf.data());
|
||||||
|
QString lineText = QString::fromUtf8(buf.constData(), len);
|
||||||
|
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
|
||||||
|
lineText.chop(1);
|
||||||
|
|
||||||
|
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
|
||||||
|
QVERIFY2(srcSpan.valid, "Source span should be valid on command row");
|
||||||
|
|
||||||
|
// Programmatically move mouse to the source span
|
||||||
|
int hoverCol = srcSpan.start + 1;
|
||||||
|
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
|
||||||
|
sendMouseMove(sci->viewport(), hoverPos);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Verify IND_HOVER_SPAN is set at the hover position
|
||||||
|
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||||
|
(unsigned long)0, (long)hoverCol);
|
||||||
|
sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT,
|
||||||
|
(unsigned long)IND_HOVER_SPAN);
|
||||||
|
int valBefore = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||||
|
(unsigned long)IND_HOVER_SPAN, pos);
|
||||||
|
QVERIFY2(valBefore != 0,
|
||||||
|
"IND_HOVER_SPAN should be set on source span after hover");
|
||||||
|
|
||||||
|
// Verify cursor is PointingHand (Source target = clickable)
|
||||||
|
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||||
|
|
||||||
|
// ── Simulate a full refresh cycle (same order as controller.refresh) ──
|
||||||
|
ViewState vs = m_editor->saveViewState();
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
m_editor->restoreViewState(vs);
|
||||||
|
|
||||||
|
// Cursor must NOT have flipped to Arrow during applyDocument
|
||||||
|
// (applyHoverCursor is not called prematurely on composed text)
|
||||||
|
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||||
|
|
||||||
|
// updateCommandRow() — replaces line 0 text
|
||||||
|
m_editor->setCommandRowText(cmdText);
|
||||||
|
|
||||||
|
// applySelectionOverlays() — must run AFTER updateCommandRow
|
||||||
|
m_editor->applySelectionOverlay(QSet<uint64_t>());
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Re-query the position (text was replaced, byte offset may have shifted)
|
||||||
|
long posAfter = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||||
|
(unsigned long)0, (long)hoverCol);
|
||||||
|
int valAfter = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||||
|
(unsigned long)IND_HOVER_SPAN, posAfter);
|
||||||
|
QVERIFY2(valAfter != 0,
|
||||||
|
"IND_HOVER_SPAN must survive refresh on command row "
|
||||||
|
"(hover should not flicker)");
|
||||||
|
|
||||||
|
// Cursor must still be PointingHand after full refresh cycle
|
||||||
|
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||||
|
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: command row hover survives multiple rapid refresh cycles ──
|
||||||
|
void testCommandRowHoverSurvivesRepeatedRefresh() {
|
||||||
|
constexpr int IND_HOVER_SPAN = 11;
|
||||||
|
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
|
QString cmdText = QStringLiteral(
|
||||||
|
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
|
||||||
|
m_editor->setCommandRowText(cmdText);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto* sci = m_editor->scintilla();
|
||||||
|
int lineLen = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||||
|
QByteArray buf(lineLen + 1, '\0');
|
||||||
|
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
|
||||||
|
(void*)buf.data());
|
||||||
|
QString lineText = QString::fromUtf8(buf.constData(), lineLen);
|
||||||
|
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
|
||||||
|
lineText.chop(1);
|
||||||
|
|
||||||
|
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
|
||||||
|
QVERIFY(srcSpan.valid);
|
||||||
|
int hoverCol = srcSpan.start + 1;
|
||||||
|
|
||||||
|
// Move mouse into position
|
||||||
|
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
|
||||||
|
sendMouseMove(sci->viewport(), hoverPos);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Simulate 5 rapid refresh cycles (like ~660ms timer x5)
|
||||||
|
for (int cycle = 0; cycle < 5; cycle++) {
|
||||||
|
ViewState vs = m_editor->saveViewState();
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
m_editor->restoreViewState(vs);
|
||||||
|
m_editor->setCommandRowText(cmdText);
|
||||||
|
m_editor->applySelectionOverlay(QSet<uint64_t>());
|
||||||
|
|
||||||
|
// Re-send mouse move each cycle (mouse is still there physically)
|
||||||
|
sendMouseMove(sci->viewport(), hoverPos);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||||
|
(unsigned long)0, (long)hoverCol);
|
||||||
|
int val = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||||
|
(unsigned long)IND_HOVER_SPAN, pos);
|
||||||
|
QVERIFY2(val != 0,
|
||||||
|
qPrintable(QString(
|
||||||
|
"IND_HOVER_SPAN lost on refresh cycle %1").arg(cycle)));
|
||||||
|
QVERIFY2(viewportCursor(m_editor) == Qt::PointingHandCursor,
|
||||||
|
qPrintable(QString(
|
||||||
|
"Cursor flipped away from PointingHand on cycle %1").arg(cycle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Test: MenuBarStyle gives QMenu items generous click targets ──
|
// ── Test: MenuBarStyle gives QMenu items generous click targets ──
|
||||||
// ── Test: M_ACCENT marker appears on selected rows ──
|
// ── Test: M_ACCENT marker appears on selected rows ──
|
||||||
void testAccentMarkerOnSelectedRows() {
|
void testAccentMarkerOnSelectedRows() {
|
||||||
@@ -1182,6 +1255,157 @@ private slots:
|
|||||||
.arg(styled.height()).arg(base.height())));
|
.arg(styled.height()).arg(base.height())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: non-hex nodes don't show false heat coloring after offset shift ──
|
||||||
|
void testDeleteClearsHeatOnShiftedNodes() {
|
||||||
|
// Heat indicator constants (replicated from editor.cpp)
|
||||||
|
constexpr int IND_HEAT_COLD = 13;
|
||||||
|
constexpr int IND_HEAT_WARM = 17;
|
||||||
|
constexpr int IND_HEAT_HOT = 18;
|
||||||
|
|
||||||
|
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0x1000;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.structTypeName = "SmallStruct";
|
||||||
|
root.name = "s";
|
||||||
|
root.parentId = 0;
|
||||||
|
root.offset = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// field0: UInt32 at offset 0 (4 bytes) — will be deleted
|
||||||
|
// field1: UInt32 at offset 4 (4 bytes) — regular type, will shift
|
||||||
|
// field2: Float at offset 8 (4 bytes) — regular type, will shift
|
||||||
|
// field3: Hex32 at offset 12 (4 bytes) — hex type, will shift
|
||||||
|
struct FieldDef { int off; NodeKind kind; const char* name; };
|
||||||
|
FieldDef defs[] = {
|
||||||
|
{ 0, NodeKind::UInt32, "count"},
|
||||||
|
{ 4, NodeKind::UInt32, "flags"},
|
||||||
|
{ 8, NodeKind::Float, "speed"},
|
||||||
|
{12, NodeKind::Hex32, "raw"},
|
||||||
|
};
|
||||||
|
QVector<uint64_t> fieldIds;
|
||||||
|
for (auto& d : defs) {
|
||||||
|
Node n;
|
||||||
|
n.kind = d.kind;
|
||||||
|
n.name = d.name;
|
||||||
|
n.parentId = rootId;
|
||||||
|
n.offset = d.off;
|
||||||
|
int idx = tree.addNode(n);
|
||||||
|
fieldIds.append(tree.nodes[idx].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a provider with 16 bytes of recognizable data
|
||||||
|
QByteArray data(16, '\0');
|
||||||
|
uint32_t v0 = 42; memcpy(data.data() + 0, &v0, 4); // count=42
|
||||||
|
uint32_t v1 = 0xFF; memcpy(data.data() + 4, &v1, 4); // flags=255
|
||||||
|
float v2 = 3.14f; memcpy(data.data() + 8, &v2, 4); // speed=3.14
|
||||||
|
uint32_t v3 = 0xCAFE; memcpy(data.data() + 12, &v3, 4); // raw=0xCAFE
|
||||||
|
BufferProvider prov(data);
|
||||||
|
|
||||||
|
// Compose the initial document
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
|
// Inject heatLevel=2 (warm) on field1, field2, field3 — simulates
|
||||||
|
// heat accumulated before the delete
|
||||||
|
for (auto& lm : result.meta) {
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
if (lm.nodeId == fieldIds[i])
|
||||||
|
lm.heatLevel = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply to editor — heat indicators should appear
|
||||||
|
m_editor->applyDocument(result);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
auto* sci = m_editor->scintilla();
|
||||||
|
|
||||||
|
// Helper: check if any heat indicator is set anywhere on a line
|
||||||
|
auto hasHeatOnLine = [&](int line) -> bool {
|
||||||
|
int lineLen = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
||||||
|
long lineStart = sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
|
||||||
|
for (long pos = lineStart; pos < lineStart + lineLen; pos++) {
|
||||||
|
for (int ind : { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT }) {
|
||||||
|
int val = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||||
|
(unsigned long)ind, pos);
|
||||||
|
if (val != 0) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find lines for each shifted field
|
||||||
|
auto findFieldLine = [&](const ComposeResult& cr, uint64_t nodeId) -> int {
|
||||||
|
for (int i = 0; i < cr.meta.size(); i++) {
|
||||||
|
if (cr.meta[i].nodeId == nodeId && cr.meta[i].lineKind == LineKind::Field)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
int line1 = findFieldLine(result, fieldIds[1]);
|
||||||
|
int line2 = findFieldLine(result, fieldIds[2]);
|
||||||
|
int line3 = findFieldLine(result, fieldIds[3]);
|
||||||
|
QVERIFY(line1 >= 0);
|
||||||
|
QVERIFY(line2 >= 0);
|
||||||
|
QVERIFY(line3 >= 0);
|
||||||
|
|
||||||
|
// Verify heat indicators ARE present (UInt32, Float, and Hex32)
|
||||||
|
QVERIFY2(hasHeatOnLine(line1),
|
||||||
|
"Heat should be present on UInt32 'flags' before delete");
|
||||||
|
QVERIFY2(hasHeatOnLine(line2),
|
||||||
|
"Heat should be present on Float 'speed' before delete");
|
||||||
|
QVERIFY2(hasHeatOnLine(line3),
|
||||||
|
"Heat should be present on Hex32 'raw' before delete");
|
||||||
|
|
||||||
|
// ── Simulate delete of field0 (UInt32 'count' at offset 0) ──
|
||||||
|
int field0Idx = tree.indexOfId(fieldIds[0]);
|
||||||
|
QVERIFY(field0Idx >= 0);
|
||||||
|
tree.nodes.remove(field0Idx);
|
||||||
|
tree.invalidateIdCache();
|
||||||
|
|
||||||
|
// Shift remaining fields' offsets down by 4
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
int fi = tree.indexOfId(fieldIds[i]);
|
||||||
|
if (fi >= 0) tree.nodes[fi].offset -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompose — heatLevel defaults to 0 (simulates cleared history)
|
||||||
|
ComposeResult afterResult = compose(tree, prov);
|
||||||
|
|
||||||
|
// Apply the post-delete document to the editor
|
||||||
|
m_editor->applyDocument(afterResult);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find new line positions
|
||||||
|
int newLine1 = findFieldLine(afterResult, fieldIds[1]);
|
||||||
|
int newLine2 = findFieldLine(afterResult, fieldIds[2]);
|
||||||
|
int newLine3 = findFieldLine(afterResult, fieldIds[3]);
|
||||||
|
QVERIFY(newLine1 >= 0);
|
||||||
|
QVERIFY(newLine2 >= 0);
|
||||||
|
QVERIFY(newLine3 >= 0);
|
||||||
|
|
||||||
|
// After applying heatLevel=0, NO heat indicators should appear
|
||||||
|
QVERIFY2(!hasHeatOnLine(newLine1),
|
||||||
|
"UInt32 'flags' should NOT show heat after offset shift "
|
||||||
|
"(old values are from wrong address)");
|
||||||
|
QVERIFY2(!hasHeatOnLine(newLine2),
|
||||||
|
"Float 'speed' should NOT show heat after offset shift "
|
||||||
|
"(old values are from wrong address)");
|
||||||
|
QVERIFY2(!hasHeatOnLine(newLine3),
|
||||||
|
"Hex32 'raw' should NOT show heat after offset shift "
|
||||||
|
"(old values are from wrong address)");
|
||||||
|
|
||||||
|
// Restore original document
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
void testMenuHoverRendersAmberText() {
|
void testMenuHoverRendersAmberText() {
|
||||||
// Replicate MenuBarStyle with drawControl hover override
|
// Replicate MenuBarStyle with drawControl hover override
|
||||||
class TestMenuStyle : public QProxyStyle {
|
class TestMenuStyle : public QProxyStyle {
|
||||||
|
|||||||
360
tests/test_export_xml.cpp
Normal file
360
tests/test_export_xml.cpp
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include "core.h"
|
||||||
|
#include "export_reclass_xml.h"
|
||||||
|
#include "import_reclass_xml.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
class TestExportXml : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void exportEmptyTree();
|
||||||
|
void exportSingleStruct();
|
||||||
|
void exportPointerRef();
|
||||||
|
void exportEmbeddedStruct();
|
||||||
|
void exportArray();
|
||||||
|
void exportTextNodes();
|
||||||
|
void exportVectors();
|
||||||
|
void exportHexCollapse();
|
||||||
|
void exportMultiClass();
|
||||||
|
void roundTripImportExport();
|
||||||
|
};
|
||||||
|
|
||||||
|
static int countRoots(const NodeTree& tree) {
|
||||||
|
int n = 0;
|
||||||
|
for (const auto& node : tree.nodes)
|
||||||
|
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||||
|
QVector<int> result;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
|
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString exportToString(const NodeTree& tree) {
|
||||||
|
QTemporaryFile tmp;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
if (!tmp.open()) return {};
|
||||||
|
QString path = tmp.fileName();
|
||||||
|
tmp.close();
|
||||||
|
|
||||||
|
QString err;
|
||||||
|
if (!exportReclassXml(tree, path, &err)) return {};
|
||||||
|
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||||
|
return QString::fromUtf8(f.readAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
static NodeTree roundTrip(const NodeTree& tree) {
|
||||||
|
QTemporaryFile tmp;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
if (!tmp.open()) return {};
|
||||||
|
QString path = tmp.fileName();
|
||||||
|
tmp.close();
|
||||||
|
|
||||||
|
QString err;
|
||||||
|
if (!exportReclassXml(tree, path, &err)) return {};
|
||||||
|
return importReclassXml(path, &err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ──
|
||||||
|
|
||||||
|
void TestExportXml::exportEmptyTree() {
|
||||||
|
NodeTree tree;
|
||||||
|
QString err;
|
||||||
|
QVERIFY(!exportReclassXml(tree, "dummy.xml", &err));
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportSingleStruct() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Player");
|
||||||
|
s.structTypeName = QStringLiteral("Player"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node f1; f1.kind = NodeKind::Int32; f1.name = QStringLiteral("health");
|
||||||
|
f1.parentId = sid; f1.offset = 0; tree.addNode(f1);
|
||||||
|
|
||||||
|
Node f2; f2.kind = NodeKind::Float; f2.name = QStringLiteral("speed");
|
||||||
|
f2.parentId = sid; f2.offset = 4; tree.addNode(f2);
|
||||||
|
|
||||||
|
Node f3; f3.kind = NodeKind::UInt64; f3.name = QStringLiteral("id");
|
||||||
|
f3.parentId = sid; f3.offset = 8; tree.addNode(f3);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(!xml.isEmpty());
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Player")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("health")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("speed")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("ReClassEx")));
|
||||||
|
|
||||||
|
// Round-trip
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
QCOMPARE(rt.nodes[0].name, QStringLiteral("Player"));
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 3);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportPointerRef() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s1; s1.kind = NodeKind::Struct; s1.name = QStringLiteral("Target");
|
||||||
|
s1.structTypeName = QStringLiteral("Target"); s1.parentId = 0;
|
||||||
|
int s1i = tree.addNode(s1);
|
||||||
|
uint64_t s1id = tree.nodes[s1i].id;
|
||||||
|
|
||||||
|
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||||
|
f.parentId = s1id; f.offset = 0; tree.addNode(f);
|
||||||
|
|
||||||
|
Node s2; s2.kind = NodeKind::Struct; s2.name = QStringLiteral("HasPtr");
|
||||||
|
s2.structTypeName = QStringLiteral("HasPtr"); s2.parentId = 0;
|
||||||
|
int s2i = tree.addNode(s2);
|
||||||
|
uint64_t s2id = tree.nodes[s2i].id;
|
||||||
|
|
||||||
|
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("pTarget");
|
||||||
|
ptr.parentId = s2id; ptr.offset = 0; ptr.refId = s1id;
|
||||||
|
tree.addNode(ptr);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Pointer=\"Target\"")));
|
||||||
|
|
||||||
|
// Round-trip: pointer should resolve
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 2);
|
||||||
|
bool foundPtr = false;
|
||||||
|
for (const auto& n : rt.nodes) {
|
||||||
|
if (n.kind == NodeKind::Pointer64 && n.name == QStringLiteral("pTarget")) {
|
||||||
|
QVERIFY(n.refId != 0);
|
||||||
|
foundPtr = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportEmbeddedStruct() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node inner; inner.kind = NodeKind::Struct; inner.name = QStringLiteral("Inner");
|
||||||
|
inner.structTypeName = QStringLiteral("Inner"); inner.parentId = 0;
|
||||||
|
int ii = tree.addNode(inner);
|
||||||
|
uint64_t iid = tree.nodes[ii].id;
|
||||||
|
|
||||||
|
Node iv; iv.kind = NodeKind::Int32; iv.name = QStringLiteral("x");
|
||||||
|
iv.parentId = iid; iv.offset = 0; tree.addNode(iv);
|
||||||
|
|
||||||
|
Node outer; outer.kind = NodeKind::Struct; outer.name = QStringLiteral("Outer");
|
||||||
|
outer.structTypeName = QStringLiteral("Outer"); outer.parentId = 0;
|
||||||
|
int oi = tree.addNode(outer);
|
||||||
|
uint64_t oid = tree.nodes[oi].id;
|
||||||
|
|
||||||
|
Node embed; embed.kind = NodeKind::Struct; embed.name = QStringLiteral("embedded");
|
||||||
|
embed.structTypeName = QStringLiteral("Inner"); embed.parentId = oid;
|
||||||
|
embed.offset = 0; embed.refId = iid;
|
||||||
|
tree.addNode(embed);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Instance=\"Inner\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportArray() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Container");
|
||||||
|
s.structTypeName = QStringLiteral("Container"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node arr; arr.kind = NodeKind::Array; arr.name = QStringLiteral("items");
|
||||||
|
arr.parentId = sid; arr.offset = 0; arr.arrayLen = 10;
|
||||||
|
arr.elementKind = NodeKind::Int32;
|
||||||
|
tree.addNode(arr);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Total=\"10\"")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("<Array")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportTextNodes() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("TextStruct");
|
||||||
|
s.structTypeName = QStringLiteral("TextStruct"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("name");
|
||||||
|
u8.parentId = sid; u8.offset = 0; u8.strLen = 32; tree.addNode(u8);
|
||||||
|
|
||||||
|
Node u16; u16.kind = NodeKind::UTF16; u16.name = QStringLiteral("wname");
|
||||||
|
u16.parentId = sid; u16.offset = 32; u16.strLen = 16; tree.addNode(u16);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].strLen, 32);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::UTF16);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].strLen, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportVectors() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Vectors");
|
||||||
|
s.structTypeName = QStringLiteral("Vectors"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node v2; v2.kind = NodeKind::Vec2; v2.name = QStringLiteral("pos2");
|
||||||
|
v2.parentId = sid; v2.offset = 0; tree.addNode(v2);
|
||||||
|
|
||||||
|
Node v3; v3.kind = NodeKind::Vec3; v3.name = QStringLiteral("pos3");
|
||||||
|
v3.parentId = sid; v3.offset = 8; tree.addNode(v3);
|
||||||
|
|
||||||
|
Node v4; v4.kind = NodeKind::Vec4; v4.name = QStringLiteral("rot");
|
||||||
|
v4.parentId = sid; v4.offset = 20; tree.addNode(v4);
|
||||||
|
|
||||||
|
Node m; m.kind = NodeKind::Mat4x4; m.name = QStringLiteral("matrix");
|
||||||
|
m.parentId = sid; m.offset = 36; tree.addNode(m);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Vec3);
|
||||||
|
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::Vec4);
|
||||||
|
QCOMPARE(rt.nodes[kids[3]].kind, NodeKind::Mat4x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportHexCollapse() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("HexTest");
|
||||||
|
s.structTypeName = QStringLiteral("HexTest"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
// 4 consecutive Hex8 nodes should collapse to one Custom node
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
Node h; h.kind = NodeKind::Hex8; h.parentId = sid; h.offset = i;
|
||||||
|
tree.addNode(h);
|
||||||
|
}
|
||||||
|
// Followed by a real field
|
||||||
|
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||||
|
f.parentId = sid; f.offset = 4; tree.addNode(f);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
// Should have Type="21" (Custom) for the collapsed hex
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Type=\"21\"")));
|
||||||
|
// Size should be 4
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Size=\"4\"")));
|
||||||
|
|
||||||
|
// Round-trip: custom expands back to hex nodes
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
// Import expands Custom(4 bytes) to best-fit hex: Hex32 (1 node) + Int32 = 2
|
||||||
|
QVERIFY(kids.size() >= 2);
|
||||||
|
// Last child should be Int32
|
||||||
|
QCOMPARE(rt.nodes[kids.last()].kind, NodeKind::Int32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportMultiClass() {
|
||||||
|
NodeTree tree;
|
||||||
|
for (int c = 0; c < 5; c++) {
|
||||||
|
Node s; s.kind = NodeKind::Struct;
|
||||||
|
s.name = QStringLiteral("Class%1").arg(c);
|
||||||
|
s.structTypeName = s.name; s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node f; f.kind = NodeKind::Int32;
|
||||||
|
f.name = QStringLiteral("field%1").arg(c);
|
||||||
|
f.parentId = sid; f.offset = 0; tree.addNode(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 5);
|
||||||
|
|
||||||
|
// All class names preserved
|
||||||
|
QSet<QString> names;
|
||||||
|
for (const auto& n : rt.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) names.insert(n.name);
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
QVERIFY(names.contains(QStringLiteral("Class%1").arg(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::roundTripImportExport() {
|
||||||
|
// Build a comprehensive tree and verify it survives export->import
|
||||||
|
NodeTree tree;
|
||||||
|
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("FullTest");
|
||||||
|
s.structTypeName = QStringLiteral("FullTest"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
auto addField = [&](NodeKind kind, const QString& name) {
|
||||||
|
Node n; n.kind = kind; n.name = name; n.parentId = sid; n.offset = offset;
|
||||||
|
tree.addNode(n);
|
||||||
|
offset += sizeForKind(kind);
|
||||||
|
};
|
||||||
|
|
||||||
|
addField(NodeKind::Int8, QStringLiteral("a"));
|
||||||
|
addField(NodeKind::Int16, QStringLiteral("b"));
|
||||||
|
addField(NodeKind::Int32, QStringLiteral("c"));
|
||||||
|
addField(NodeKind::Int64, QStringLiteral("d"));
|
||||||
|
addField(NodeKind::UInt8, QStringLiteral("e"));
|
||||||
|
addField(NodeKind::UInt16, QStringLiteral("f"));
|
||||||
|
addField(NodeKind::UInt32, QStringLiteral("g"));
|
||||||
|
addField(NodeKind::UInt64, QStringLiteral("h"));
|
||||||
|
addField(NodeKind::Float, QStringLiteral("i"));
|
||||||
|
addField(NodeKind::Double, QStringLiteral("j"));
|
||||||
|
addField(NodeKind::Vec2, QStringLiteral("k"));
|
||||||
|
addField(NodeKind::Vec3, QStringLiteral("l"));
|
||||||
|
addField(NodeKind::Vec4, QStringLiteral("m"));
|
||||||
|
|
||||||
|
// Self-pointer
|
||||||
|
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("self");
|
||||||
|
ptr.parentId = sid; ptr.offset = offset; ptr.refId = sid;
|
||||||
|
tree.addNode(ptr);
|
||||||
|
offset += 8;
|
||||||
|
|
||||||
|
// UTF8
|
||||||
|
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("str");
|
||||||
|
u8.parentId = sid; u8.offset = offset; u8.strLen = 64;
|
||||||
|
tree.addNode(u8);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
QCOMPARE(rt.nodes[0].name, QStringLiteral("FullTest"));
|
||||||
|
|
||||||
|
auto origKids = childrenOf(tree, sid);
|
||||||
|
auto rtKids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(rtKids.size(), origKids.size());
|
||||||
|
|
||||||
|
// Verify each field kind matches
|
||||||
|
for (int i = 0; i < origKids.size(); i++) {
|
||||||
|
QCOMPARE(rt.nodes[rtKids[i]].kind, tree.nodes[origKids[i]].kind);
|
||||||
|
QCOMPARE(rt.nodes[rtKids[i]].name, tree.nodes[origKids[i]].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify self-pointer resolved
|
||||||
|
bool foundSelf = false;
|
||||||
|
for (const auto& n : rt.nodes) {
|
||||||
|
if (n.name == QStringLiteral("self") && n.kind == NodeKind::Pointer64) {
|
||||||
|
QVERIFY(n.refId != 0);
|
||||||
|
QCOMPARE(n.refId, rt.nodes[0].id);
|
||||||
|
foundSelf = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundSelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestExportXml)
|
||||||
|
#include "test_export_xml.moc"
|
||||||
@@ -418,30 +418,6 @@ private slots:
|
|||||||
QVERIFY(result.contains("wchar_t wname[32];"));
|
QVERIFY(result.contains("wchar_t wname[32];"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Padding node ──
|
|
||||||
|
|
||||||
void testPaddingNode() {
|
|
||||||
rcx::NodeTree tree;
|
|
||||||
rcx::Node root;
|
|
||||||
root.kind = rcx::NodeKind::Struct;
|
|
||||||
root.name = "PadTest";
|
|
||||||
root.structTypeName = "PadTest";
|
|
||||||
root.parentId = 0;
|
|
||||||
int ri = tree.addNode(root);
|
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
|
||||||
|
|
||||||
rcx::Node pad;
|
|
||||||
pad.kind = rcx::NodeKind::Padding;
|
|
||||||
pad.name = "reserved";
|
|
||||||
pad.parentId = rootId;
|
|
||||||
pad.offset = 0;
|
|
||||||
pad.arrayLen = 16;
|
|
||||||
tree.addNode(pad);
|
|
||||||
|
|
||||||
QString result = rcx::renderCpp(tree, rootId);
|
|
||||||
QVERIFY(result.contains("uint8_t reserved[16];"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Full SDK export (multiple root structs) ──
|
// ── Full SDK export (multiple root structs) ──
|
||||||
|
|
||||||
void testFullSdkExport() {
|
void testFullSdkExport() {
|
||||||
|
|||||||
846
tests/test_import_source.cpp
Normal file
846
tests/test_import_source.cpp
Normal file
@@ -0,0 +1,846 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include "core.h"
|
||||||
|
#include "import_source.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
class TestImportSource : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
// Basic type tests
|
||||||
|
void emptyInput();
|
||||||
|
void noStructs();
|
||||||
|
void singleEmptyStruct();
|
||||||
|
void stdintTypes();
|
||||||
|
void windowsTypes();
|
||||||
|
void platformPointerTypes();
|
||||||
|
void standardCTypes();
|
||||||
|
void multiWordTypes();
|
||||||
|
void floatDouble();
|
||||||
|
void boolType();
|
||||||
|
|
||||||
|
// Pointer tests
|
||||||
|
void voidPointer();
|
||||||
|
void typedPointer();
|
||||||
|
void selfReferencingPointer();
|
||||||
|
void doublePointer();
|
||||||
|
|
||||||
|
// Array tests
|
||||||
|
void primitiveArray();
|
||||||
|
void charArrayToUtf8();
|
||||||
|
void wcharArrayToUtf16();
|
||||||
|
void floatArrayToVec2();
|
||||||
|
void floatArrayToVec3();
|
||||||
|
void floatArrayToVec4();
|
||||||
|
void floatArray4x4ToMat4x4();
|
||||||
|
void genericFloatArray();
|
||||||
|
void structArray();
|
||||||
|
|
||||||
|
// Comment offset tests
|
||||||
|
void commentOffsets();
|
||||||
|
void computedOffsets();
|
||||||
|
void mixedOffsetsAutoDetect();
|
||||||
|
|
||||||
|
// Multi-struct tests
|
||||||
|
void multiStruct();
|
||||||
|
void pointerCrossRef();
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
void forwardDeclaration();
|
||||||
|
|
||||||
|
// Union handling
|
||||||
|
void unionPickFirst();
|
||||||
|
|
||||||
|
// Padding fields
|
||||||
|
void paddingFieldExpansion();
|
||||||
|
|
||||||
|
// static_assert
|
||||||
|
void staticAssertTailPadding();
|
||||||
|
|
||||||
|
// Embedded struct
|
||||||
|
void embeddedStruct();
|
||||||
|
|
||||||
|
// Typedef
|
||||||
|
void typedefBasic();
|
||||||
|
|
||||||
|
// Qualifiers
|
||||||
|
void constVolatileQualifiers();
|
||||||
|
void structPrefixOnType();
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
void bitfieldSkipped();
|
||||||
|
void hexArraySizes();
|
||||||
|
void windowsStylePEB();
|
||||||
|
void classKeyword();
|
||||||
|
void inheritanceSkipped();
|
||||||
|
|
||||||
|
// Round-trip test (requires generator.h)
|
||||||
|
void basicRoundTrip();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Helper ──
|
||||||
|
|
||||||
|
static int countRoots(const NodeTree& tree) {
|
||||||
|
int n = 0;
|
||||||
|
for (const auto& node : tree.nodes)
|
||||||
|
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||||
|
QVector<int> result;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
|
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ──
|
||||||
|
|
||||||
|
void TestImportSource::emptyInput() {
|
||||||
|
QString err;
|
||||||
|
NodeTree tree = importFromSource(QString(), &err);
|
||||||
|
QVERIFY(tree.nodes.isEmpty());
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::noStructs() {
|
||||||
|
QString err;
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral("int x = 42;"), &err);
|
||||||
|
QVERIFY(tree.nodes.isEmpty());
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::singleEmptyStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Empty {};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("Empty"));
|
||||||
|
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::stdintTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Test {\n"
|
||||||
|
" uint8_t a;\n"
|
||||||
|
" int8_t b;\n"
|
||||||
|
" uint16_t c;\n"
|
||||||
|
" int16_t d;\n"
|
||||||
|
" uint32_t e;\n"
|
||||||
|
" int32_t f;\n"
|
||||||
|
" uint64_t g;\n"
|
||||||
|
" int64_t h;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int8);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt16);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int16);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::Int64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::windowsTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WinTypes {\n"
|
||||||
|
" BYTE a;\n"
|
||||||
|
" WORD b;\n"
|
||||||
|
" DWORD c;\n"
|
||||||
|
" QWORD d;\n"
|
||||||
|
" ULONG e;\n"
|
||||||
|
" LONG f;\n"
|
||||||
|
" USHORT g;\n"
|
||||||
|
" UCHAR h;\n"
|
||||||
|
" BOOLEAN i;\n"
|
||||||
|
" BOOL j;\n"
|
||||||
|
" CHAR k;\n"
|
||||||
|
" WCHAR l;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 12);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8); // BYTE
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16); // WORD
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32); // DWORD
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64); // QWORD
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32); // ULONG
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32); // LONG
|
||||||
|
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt16); // USHORT
|
||||||
|
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::UInt8); // UCHAR
|
||||||
|
QCOMPARE(tree.nodes[kids[8]].kind, NodeKind::UInt8); // BOOLEAN
|
||||||
|
QCOMPARE(tree.nodes[kids[9]].kind, NodeKind::Int32); // BOOL
|
||||||
|
QCOMPARE(tree.nodes[kids[10]].kind, NodeKind::Int8); // CHAR
|
||||||
|
QCOMPARE(tree.nodes[kids[11]].kind, NodeKind::UInt16); // WCHAR
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::platformPointerTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PtrTypes {\n"
|
||||||
|
" PVOID a;\n"
|
||||||
|
" HANDLE b;\n"
|
||||||
|
" SIZE_T c;\n"
|
||||||
|
" ULONG_PTR d;\n"
|
||||||
|
" uintptr_t e;\n"
|
||||||
|
" size_t f;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::standardCTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct CTypes {\n"
|
||||||
|
" char a;\n"
|
||||||
|
" short b;\n"
|
||||||
|
" int c;\n"
|
||||||
|
" long d;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Int8); // char
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int16); // short
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::Int32); // int
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int32); // long
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::multiWordTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct MultiWord {\n"
|
||||||
|
" unsigned char a;\n"
|
||||||
|
" unsigned short b;\n"
|
||||||
|
" unsigned int c;\n"
|
||||||
|
" unsigned long d;\n"
|
||||||
|
" long long e;\n"
|
||||||
|
" unsigned long long f;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Int64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatDouble() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct FD {\n"
|
||||||
|
" float a;\n"
|
||||||
|
" double b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Double);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::boolType() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct B {\n"
|
||||||
|
" bool a;\n"
|
||||||
|
" _Bool b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Bool);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::voidPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct VP {\n"
|
||||||
|
" void* ptr;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("ptr"));
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].refId, uint64_t(0)); // void* has no target
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::typedPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Target {\n"
|
||||||
|
" int x;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct HasPtr {\n"
|
||||||
|
" Target* pTarget;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Find HasPtr
|
||||||
|
int hasPtrIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("HasPtr") && tree.nodes[i].parentId == 0) {
|
||||||
|
hasPtrIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(hasPtrIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[hasPtrIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
// refId should point to Target struct
|
||||||
|
int targetIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||||
|
QVERIFY(targetIdx >= 0);
|
||||||
|
QCOMPARE(tree.nodes[targetIdx].name, QStringLiteral("Target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::selfReferencingPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Node {\n"
|
||||||
|
" int value;\n"
|
||||||
|
" Node* next;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].refId, tree.nodes[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::doublePointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct DP {\n"
|
||||||
|
" void** ppData;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::primitiveArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PA {\n"
|
||||||
|
" int32_t values[10];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 10);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Int32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::charArrayToUtf8() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct CA {\n"
|
||||||
|
" char name[64];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].strLen, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::wcharArrayToUtf16() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WC {\n"
|
||||||
|
" wchar_t name[32];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF16);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].strLen, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec2() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float pos[2];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec3() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float pos[3];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec4() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float rot[4];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArray4x4ToMat4x4() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct M {\n"
|
||||||
|
" float matrix[4][4];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Mat4x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::genericFloatArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct GF {\n"
|
||||||
|
" float values[8];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::structArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Item {\n"
|
||||||
|
" int id;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Container {\n"
|
||||||
|
" Item items[5];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Find Container
|
||||||
|
int contIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Container") && tree.nodes[i].parentId == 0) {
|
||||||
|
contIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(contIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[contIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 5);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Struct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::commentOffsets() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Offsets {\n"
|
||||||
|
" uint64_t vtable; // 0x0\n"
|
||||||
|
" float health; // 0x8\n"
|
||||||
|
" uint8_t _pad000C[0x4]; // 0xC\n"
|
||||||
|
" double score; // 0x10\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// vtable at 0x0
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt64);
|
||||||
|
// health at 0x8
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
// _pad at 0xC -> hex nodes
|
||||||
|
// score at 0x10
|
||||||
|
// Find the double
|
||||||
|
bool foundDouble = false;
|
||||||
|
for (int k : kids) {
|
||||||
|
if (tree.nodes[k].kind == NodeKind::Double) {
|
||||||
|
QCOMPARE(tree.nodes[k].offset, 0x10);
|
||||||
|
foundDouble = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundDouble);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::computedOffsets() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Computed {\n"
|
||||||
|
" uint8_t a;\n"
|
||||||
|
" uint16_t b;\n"
|
||||||
|
" uint32_t c;\n"
|
||||||
|
" uint64_t d;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0); // uint8_t at 0
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 1); // uint16_t at 1
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].offset, 3); // uint32_t at 3
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].offset, 7); // uint64_t at 7
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::mixedOffsetsAutoDetect() {
|
||||||
|
// If any field has a comment offset, all should use comment mode
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Mixed {\n"
|
||||||
|
" uint32_t a; // 0x0\n"
|
||||||
|
" uint32_t b;\n"
|
||||||
|
" uint32_t c; // 0x10\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
// b has no comment offset, in comment mode it gets computed offset 4
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||||
|
// c has comment offset 0x10
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].offset, 0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::multiStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct A {\n"
|
||||||
|
" int x;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct B {\n"
|
||||||
|
" float y;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct C {\n"
|
||||||
|
" double z;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::pointerCrossRef() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct A {\n"
|
||||||
|
" int value;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct B {\n"
|
||||||
|
" A* ref;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
// Find B's pointer field
|
||||||
|
int bIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("B") && tree.nodes[i].parentId == 0) {
|
||||||
|
bIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(bIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[bIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
// Should point to A
|
||||||
|
int aIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||||
|
QVERIFY(aIdx >= 0);
|
||||||
|
QCOMPARE(tree.nodes[aIdx].name, QStringLiteral("A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::forwardDeclaration() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Bar;\n"
|
||||||
|
"struct Foo {\n"
|
||||||
|
" Bar* pBar;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Bar {\n"
|
||||||
|
" int val;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Foo's pBar should resolve to Bar
|
||||||
|
int fooIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Foo") && tree.nodes[i].parentId == 0) {
|
||||||
|
fooIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(fooIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::unionPickFirst() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WithUnion {\n"
|
||||||
|
" union {\n"
|
||||||
|
" float asFloat;\n"
|
||||||
|
" uint32_t asInt;\n"
|
||||||
|
" };\n"
|
||||||
|
" int after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// Should have 2 fields: asFloat (first union member) + after
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::paddingFieldExpansion() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Padded {\n"
|
||||||
|
" uint8_t _pad0000[0x10];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// 0x10 = 16 bytes, should be 2x Hex64 (best fit)
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Hex64);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::staticAssertTailPadding() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Sized {\n"
|
||||||
|
" uint32_t x;\n"
|
||||||
|
"};\n"
|
||||||
|
"static_assert(sizeof(Sized) == 0x10, \"Size check\");\n"
|
||||||
|
));
|
||||||
|
// x is 4 bytes, static_assert says 0x10 = 16
|
||||||
|
// Should have tail padding from offset 4 to 16 (12 bytes)
|
||||||
|
int span = tree.structSpan(tree.nodes[0].id);
|
||||||
|
QCOMPARE(span, 0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::embeddedStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Inner {\n"
|
||||||
|
" int a;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Outer {\n"
|
||||||
|
" Inner embedded;\n"
|
||||||
|
" float after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
int outerIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||||
|
outerIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(outerIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::typedefBasic() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"typedef uint32_t MyInt;\n"
|
||||||
|
"struct TD {\n"
|
||||||
|
" MyInt value;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::constVolatileQualifiers() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Quals {\n"
|
||||||
|
" const uint32_t a;\n"
|
||||||
|
" volatile int32_t b;\n"
|
||||||
|
" const volatile uint8_t c;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 3);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::structPrefixOnType() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Inner {\n"
|
||||||
|
" int val;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Outer {\n"
|
||||||
|
" struct Inner member;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
int outerIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||||
|
outerIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(outerIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::bitfieldSkipped() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct BF {\n"
|
||||||
|
" uint32_t normal;\n"
|
||||||
|
" uint32_t bitA : 4;\n"
|
||||||
|
" uint32_t bitB : 12;\n"
|
||||||
|
" uint32_t after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// Bitfields should be skipped, only normal + after
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::hexArraySizes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct HexArr {\n"
|
||||||
|
" uint8_t data[0x20];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 0x20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::windowsStylePEB() {
|
||||||
|
// Test with Windows PEB-style struct (no comment offsets)
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PEB64 {\n"
|
||||||
|
" BOOLEAN InheritedAddressSpace;\n"
|
||||||
|
" BOOLEAN ReadImageFileExecOptions;\n"
|
||||||
|
" BOOLEAN BeingDebugged;\n"
|
||||||
|
" BOOLEAN BitField;\n"
|
||||||
|
" PVOID Mutant;\n"
|
||||||
|
" PVOID ImageBaseAddress;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("PEB64"));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
// First 4 are BOOLEAN (UInt8)
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
QCOMPARE(tree.nodes[kids[i]].kind, NodeKind::UInt8);
|
||||||
|
// Last 2 are PVOID (Pointer64)
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Pointer64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::classKeyword() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"class MyClass {\n"
|
||||||
|
" int value;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("class"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::inheritanceSkipped() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Base {\n"
|
||||||
|
" int a;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Derived : public Base {\n"
|
||||||
|
" float b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
int derivedIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Derived") && tree.nodes[i].parentId == 0) {
|
||||||
|
derivedIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(derivedIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[derivedIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::basicRoundTrip() {
|
||||||
|
// Build a simple tree manually, export it, then re-import and compare
|
||||||
|
NodeTree original;
|
||||||
|
{
|
||||||
|
Node s;
|
||||||
|
s.kind = NodeKind::Struct;
|
||||||
|
s.name = QStringLiteral("RoundTrip");
|
||||||
|
s.structTypeName = QStringLiteral("RoundTrip");
|
||||||
|
s.parentId = 0;
|
||||||
|
s.offset = 0;
|
||||||
|
int sIdx = original.addNode(s);
|
||||||
|
uint64_t sId = original.nodes[sIdx].id;
|
||||||
|
|
||||||
|
Node f1;
|
||||||
|
f1.kind = NodeKind::UInt32;
|
||||||
|
f1.name = QStringLiteral("field_a");
|
||||||
|
f1.parentId = sId;
|
||||||
|
f1.offset = 0;
|
||||||
|
original.addNode(f1);
|
||||||
|
|
||||||
|
Node f2;
|
||||||
|
f2.kind = NodeKind::Float;
|
||||||
|
f2.name = QStringLiteral("field_b");
|
||||||
|
f2.parentId = sId;
|
||||||
|
f2.offset = 4;
|
||||||
|
original.addNode(f2);
|
||||||
|
|
||||||
|
Node f3;
|
||||||
|
f3.kind = NodeKind::UInt64;
|
||||||
|
f3.name = QStringLiteral("field_c");
|
||||||
|
f3.parentId = sId;
|
||||||
|
f3.offset = 8;
|
||||||
|
original.addNode(f3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create source text that matches what generator would produce
|
||||||
|
QString source = QStringLiteral(
|
||||||
|
"struct RoundTrip {\n"
|
||||||
|
" uint32_t field_a; // 0x0\n"
|
||||||
|
" float field_b; // 0x4\n"
|
||||||
|
" uint64_t field_c; // 0x8\n"
|
||||||
|
"};\n"
|
||||||
|
"static_assert(sizeof(RoundTrip) == 0x10, \"Size mismatch\");\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
NodeTree reimported = importFromSource(source);
|
||||||
|
QCOMPARE(countRoots(reimported), 1);
|
||||||
|
QCOMPARE(reimported.nodes[0].name, QStringLiteral("RoundTrip"));
|
||||||
|
|
||||||
|
auto origKids = childrenOf(original, original.nodes[0].id);
|
||||||
|
auto reimpKids = childrenOf(reimported, reimported.nodes[0].id);
|
||||||
|
|
||||||
|
// Compare field count (reimported may have extra padding nodes from static_assert)
|
||||||
|
// Check that the first 3 fields match
|
||||||
|
QVERIFY(reimpKids.size() >= 3);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].kind, original.nodes[origKids[i]].kind);
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].name, original.nodes[origKids[i]].name);
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].offset, original.nodes[origKids[i]].offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestImportSource)
|
||||||
|
#include "test_import_source.moc"
|
||||||
70
tests/test_import_xml.cpp
Normal file
70
tests/test_import_xml.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include "core.h"
|
||||||
|
#include "import_reclass_xml.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
class TestImportXml : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void importSmallXml();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestImportXml::importSmallXml() {
|
||||||
|
// Create a minimal XML in a temp file and test parsing
|
||||||
|
QTemporaryFile tmp;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
QVERIFY(tmp.open());
|
||||||
|
tmp.write(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ReClass>
|
||||||
|
<!--ReClassEx-->
|
||||||
|
<Class Name="TestClass" Type="28" Comment="" Offset="0" strOffset="0" Code="">
|
||||||
|
<Node Name="vtable" Type="9" Size="8" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="health" Type="13" Size="4" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="name" Type="18" Size="32" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="position" Type="23" Size="12" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="pNext" Type="8" Size="8" bHidden="false" Comment="" Pointer="TestClass"/>
|
||||||
|
</Class>
|
||||||
|
</ReClass>
|
||||||
|
)");
|
||||||
|
tmp.flush();
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
NodeTree tree = importReclassXml(tmp.fileName(), &error);
|
||||||
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||||
|
|
||||||
|
// Should have 1 root struct + 5 children = 6 nodes
|
||||||
|
QCOMPARE(tree.nodes.size(), 6);
|
||||||
|
|
||||||
|
// Root struct
|
||||||
|
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("TestClass"));
|
||||||
|
|
||||||
|
// vtable = Int64
|
||||||
|
QCOMPARE(tree.nodes[1].kind, NodeKind::Int64);
|
||||||
|
QCOMPARE(tree.nodes[1].name, QStringLiteral("vtable"));
|
||||||
|
QCOMPARE(tree.nodes[1].offset, 0);
|
||||||
|
|
||||||
|
// health = Float
|
||||||
|
QCOMPARE(tree.nodes[2].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[2].name, QStringLiteral("health"));
|
||||||
|
QCOMPARE(tree.nodes[2].offset, 8);
|
||||||
|
|
||||||
|
// name = UTF8 with strLen=32
|
||||||
|
QCOMPARE(tree.nodes[3].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(tree.nodes[3].strLen, 32);
|
||||||
|
QCOMPARE(tree.nodes[3].offset, 12);
|
||||||
|
|
||||||
|
// position = Vec3
|
||||||
|
QCOMPARE(tree.nodes[4].kind, NodeKind::Vec3);
|
||||||
|
QCOMPARE(tree.nodes[4].offset, 44);
|
||||||
|
|
||||||
|
// pNext = Pointer64 with resolved refId
|
||||||
|
QCOMPARE(tree.nodes[5].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[5].name, QStringLiteral("pNext"));
|
||||||
|
QVERIFY(tree.nodes[5].refId != 0);
|
||||||
|
QCOMPARE(tree.nodes[5].refId, tree.nodes[0].id); // points to TestClass
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestImportXml)
|
||||||
|
#include "test_import_xml.moc"
|
||||||
@@ -304,39 +304,6 @@ private slots:
|
|||||||
QVERIFY(result.contains("float speed;"));
|
QVERIFY(result.contains("float speed;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void testGenerator_typeAliases_padding() {
|
|
||||||
// Padding gap and tail padding should use aliased uint8_t
|
|
||||||
NodeTree tree;
|
|
||||||
Node root;
|
|
||||||
root.kind = NodeKind::Struct;
|
|
||||||
root.name = "PadTest";
|
|
||||||
root.structTypeName = "PadTest";
|
|
||||||
root.parentId = 0;
|
|
||||||
int ri = tree.addNode(root);
|
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
|
||||||
|
|
||||||
Node f1;
|
|
||||||
f1.kind = NodeKind::UInt32;
|
|
||||||
f1.name = "a";
|
|
||||||
f1.parentId = rootId;
|
|
||||||
f1.offset = 0;
|
|
||||||
tree.addNode(f1);
|
|
||||||
|
|
||||||
Node f2;
|
|
||||||
f2.kind = NodeKind::UInt32;
|
|
||||||
f2.name = "b";
|
|
||||||
f2.parentId = rootId;
|
|
||||||
f2.offset = 8; // gap of 4 bytes at offset 4
|
|
||||||
tree.addNode(f2);
|
|
||||||
|
|
||||||
QHash<NodeKind, QString> aliases;
|
|
||||||
aliases[NodeKind::Padding] = "BYTE";
|
|
||||||
|
|
||||||
QString result = renderCpp(tree, rootId, &aliases);
|
|
||||||
// Padding gap should use the alias
|
|
||||||
QVERIFY(result.contains("BYTE _pad"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void testGenerator_typeAliases_array() {
|
void testGenerator_typeAliases_array() {
|
||||||
// Array element type should use alias
|
// Array element type should use alias
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
@@ -547,134 +514,92 @@ private slots:
|
|||||||
void testWorkspace_simpleTree() {
|
void testWorkspace_simpleTree() {
|
||||||
auto tree = makeSimpleTree();
|
auto tree = makeSimpleTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "TestProject.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
// 1 top-level item (the project)
|
// Single "Project" root
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 1);
|
||||||
QStandardItem* project = model.item(0);
|
QStandardItem* project = model.item(0);
|
||||||
QCOMPARE(project->text(), QString("TestProject.rcx"));
|
QCOMPARE(project->text(), QString("Project"));
|
||||||
|
|
||||||
// Project has 1 child: the Player struct
|
// 1 type directly under Project: Player (no member fields)
|
||||||
QCOMPARE(project->rowCount(), 1);
|
QCOMPARE(project->rowCount(), 1);
|
||||||
QStandardItem* player = project->child(0);
|
QVERIFY(project->child(0)->text().contains("Player"));
|
||||||
QVERIFY(player->text().contains("Player"));
|
QVERIFY(project->child(0)->text().contains("struct"));
|
||||||
QVERIFY(player->text().contains("struct"));
|
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||||
|
|
||||||
// Player struct has 2 children: health, speed
|
|
||||||
QCOMPARE(player->rowCount(), 2);
|
|
||||||
QVERIFY(player->child(0)->text().contains("health"));
|
|
||||||
QVERIFY(player->child(1)->text().contains("speed"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_twoRootTree() {
|
void testWorkspace_twoRootTree() {
|
||||||
auto tree = makeTwoRootTree();
|
auto tree = makeTwoRootTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "TwoRoot.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 1);
|
||||||
QStandardItem* project = model.item(0);
|
QStandardItem* project = model.item(0);
|
||||||
|
|
||||||
// 2 root struct children: Alpha and Bravo
|
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
||||||
QCOMPARE(project->rowCount(), 2);
|
QCOMPARE(project->rowCount(), 2);
|
||||||
QVERIFY(project->child(0)->text().contains("Alpha"));
|
QVERIFY(project->child(0)->text().contains("Alpha"));
|
||||||
QVERIFY(project->child(1)->text().contains("Bravo"));
|
QVERIFY(project->child(1)->text().contains("Bravo"));
|
||||||
|
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||||
// Each has 1 field child
|
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||||
QCOMPARE(project->child(0)->rowCount(), 1);
|
|
||||||
QVERIFY(project->child(0)->child(0)->text().contains("flagsA"));
|
|
||||||
QCOMPARE(project->child(1)->rowCount(), 1);
|
|
||||||
QVERIFY(project->child(1)->child(0)->text().contains("flagsB"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_richTree_rootCount() {
|
void testWorkspace_richTree_rootCount() {
|
||||||
auto tree = makeRichTree();
|
auto tree = makeRichTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* project = model.item(0);
|
QStandardItem* project = model.item(0);
|
||||||
QCOMPARE(project->rowCount(), 3); // Pet, Cat, Ball
|
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_richTree_petChildren() {
|
void testWorkspace_richTree_sorted() {
|
||||||
auto tree = makeRichTree();
|
auto tree = makeRichTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* pet = model.item(0)->child(0);
|
QStandardItem* project = model.item(0);
|
||||||
QVERIFY(pet->text().contains("Pet"));
|
// Sorted alphabetically: Ball, Cat, Pet
|
||||||
// Pet has 2 non-hex children: name (UTF8), owner (Pointer64)
|
QVERIFY(project->child(0)->text().contains("Ball"));
|
||||||
QCOMPARE(pet->rowCount(), 2);
|
QVERIFY(project->child(1)->text().contains("Cat"));
|
||||||
QVERIFY(pet->child(0)->text().contains("name"));
|
QVERIFY(project->child(2)->text().contains("Pet"));
|
||||||
QVERIFY(pet->child(1)->text().contains("owner"));
|
// No member fields under type nodes
|
||||||
}
|
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||||
|
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||||
void testWorkspace_richTree_catNesting() {
|
QCOMPARE(project->child(2)->rowCount(), 0);
|
||||||
auto tree = makeRichTree();
|
|
||||||
QStandardItemModel model;
|
|
||||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
|
||||||
|
|
||||||
QStandardItem* cat = model.item(0)->child(1);
|
|
||||||
QVERIFY(cat->text().contains("Cat"));
|
|
||||||
|
|
||||||
// Find the nested "Pet" struct child (base)
|
|
||||||
QStandardItem* base = nullptr;
|
|
||||||
for (int i = 0; i < cat->rowCount(); i++) {
|
|
||||||
if (cat->child(i)->text().contains("Pet") &&
|
|
||||||
cat->child(i)->text().contains("struct")) {
|
|
||||||
base = cat->child(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY2(base != nullptr, "Cat should have a nested Pet struct child");
|
|
||||||
|
|
||||||
// base has structId set
|
|
||||||
QVERIFY(base->data(Qt::UserRole + 1).isValid());
|
|
||||||
|
|
||||||
// base should have its own children (name + owner)
|
|
||||||
QCOMPARE(base->rowCount(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void testWorkspace_richTree_ballChildren() {
|
|
||||||
auto tree = makeRichTree();
|
|
||||||
QStandardItemModel model;
|
|
||||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
|
||||||
|
|
||||||
QStandardItem* ball = model.item(0)->child(2);
|
|
||||||
QVERIFY(ball->text().contains("Ball"));
|
|
||||||
|
|
||||||
// Ball has 3 non-hex children: speed, position, color
|
|
||||||
QCOMPARE(ball->rowCount(), 3);
|
|
||||||
QVERIFY(ball->child(0)->text().contains("speed"));
|
|
||||||
QVERIFY(ball->child(1)->text().contains("position"));
|
|
||||||
QVERIFY(ball->child(2)->text().contains("color"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_emptyTree() {
|
void testWorkspace_emptyTree() {
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "Empty.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
|
// Still has the "Project" root, just no children
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 1);
|
||||||
|
QCOMPARE(model.item(0)->text(), QString("Project"));
|
||||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
QCOMPARE(model.item(0)->rowCount(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWorkspace_structIdRole() {
|
void testWorkspace_structIdRole() {
|
||||||
auto tree = makeSimpleTree();
|
auto tree = makeSimpleTree();
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildWorkspaceModel(&model, tree, "Test.rcx");
|
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||||
|
buildProjectExplorer(&model, tabs);
|
||||||
|
|
||||||
QStandardItem* project = model.item(0);
|
QStandardItem* project = model.item(0);
|
||||||
// Project item should NOT have structId
|
// Project root has kGroupSentinel
|
||||||
QVERIFY(!project->data(Qt::UserRole + 1).isValid());
|
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||||
|
|
||||||
// Player struct should have structId
|
// Player type item should have structId
|
||||||
QStandardItem* player = project->child(0);
|
QStandardItem* player = project->child(0);
|
||||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||||
|
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||||
// health field should NOT have structId
|
|
||||||
QStandardItem* health = player->child(0);
|
|
||||||
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
|
|||||||
291
tests/test_options_dialog.cpp
Normal file
291
tests/test_options_dialog.cpp
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
#include <QtTest/QTest>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include "optionsdialog.h"
|
||||||
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
// Helper: apply the global palette the same way main.cpp does
|
||||||
|
static void applyGlobalTheme(const Theme& theme) {
|
||||||
|
QPalette pal;
|
||||||
|
pal.setColor(QPalette::Window, theme.background);
|
||||||
|
pal.setColor(QPalette::WindowText, theme.text);
|
||||||
|
pal.setColor(QPalette::Base, theme.background);
|
||||||
|
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||||
|
pal.setColor(QPalette::Text, theme.text);
|
||||||
|
pal.setColor(QPalette::Button, theme.button);
|
||||||
|
pal.setColor(QPalette::ButtonText, theme.text);
|
||||||
|
pal.setColor(QPalette::Highlight, theme.selection);
|
||||||
|
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||||
|
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||||
|
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||||
|
pal.setColor(QPalette::Mid, theme.border);
|
||||||
|
pal.setColor(QPalette::Dark, theme.background);
|
||||||
|
pal.setColor(QPalette::Light, theme.textFaint);
|
||||||
|
pal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||||
|
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Light, theme.background);
|
||||||
|
|
||||||
|
qApp->setPalette(pal);
|
||||||
|
qApp->setStyleSheet(QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestOptionsDialog : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
|
||||||
|
void initTestCase() {
|
||||||
|
// Apply theme palette so dialog inherits real colors
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
applyGlobalTheme(tm.current());
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogCreatesAllWidgets() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
defaults.themeIndex = 0;
|
||||||
|
defaults.fontName = "JetBrains Mono";
|
||||||
|
defaults.menuBarTitleCase = true;
|
||||||
|
defaults.safeMode = false;
|
||||||
|
defaults.autoStartMcp = false;
|
||||||
|
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
// Core widgets exist
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
QVERIFY(tree);
|
||||||
|
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||||
|
QVERIFY(pages);
|
||||||
|
QCOMPARE(pages->count(), 3);
|
||||||
|
|
||||||
|
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||||
|
QVERIFY(themeCombo);
|
||||||
|
QVERIFY(themeCombo->count() >= 3);
|
||||||
|
|
||||||
|
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||||
|
QVERIFY(fontCombo);
|
||||||
|
QCOMPARE(fontCombo->count(), 2);
|
||||||
|
|
||||||
|
auto* showIconCheck = dlg.findChild<QCheckBox*>();
|
||||||
|
QVERIFY(showIconCheck);
|
||||||
|
|
||||||
|
auto* buttons = dlg.findChild<QDialogButtonBox*>();
|
||||||
|
QVERIFY(buttons);
|
||||||
|
QVERIFY(buttons->button(QDialogButtonBox::Ok));
|
||||||
|
QVERIFY(buttons->button(QDialogButtonBox::Cancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
void resultReflectsInput() {
|
||||||
|
OptionsResult input;
|
||||||
|
input.themeIndex = 1;
|
||||||
|
input.fontName = "Consolas";
|
||||||
|
input.menuBarTitleCase = false;
|
||||||
|
input.safeMode = true;
|
||||||
|
input.autoStartMcp = true;
|
||||||
|
|
||||||
|
OptionsDialog dlg(input);
|
||||||
|
auto r = dlg.result();
|
||||||
|
|
||||||
|
QCOMPARE(r.themeIndex, 1);
|
||||||
|
QCOMPARE(r.fontName, QString("Consolas"));
|
||||||
|
QCOMPARE(r.menuBarTitleCase, false);
|
||||||
|
QCOMPARE(r.safeMode, true);
|
||||||
|
QCOMPARE(r.autoStartMcp, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void noStyleSheetOnDialog() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
// Dialog itself must have no stylesheet override
|
||||||
|
QVERIFY(dlg.styleSheet().isEmpty());
|
||||||
|
|
||||||
|
// Combo boxes must have no stylesheet override
|
||||||
|
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||||
|
QVERIFY(themeCombo->styleSheet().isEmpty());
|
||||||
|
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||||
|
QVERIFY(fontCombo->styleSheet().isEmpty());
|
||||||
|
|
||||||
|
// No child widget should have a stylesheet set
|
||||||
|
for (auto* child : dlg.findChildren<QWidget*>()) {
|
||||||
|
QVERIFY2(child->styleSheet().isEmpty(),
|
||||||
|
qPrintable(QString("Widget %1 (%2) has unexpected stylesheet: %3")
|
||||||
|
.arg(child->objectName(),
|
||||||
|
child->metaObject()->className(),
|
||||||
|
child->styleSheet())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void highlightColorDiffersFromBackground() {
|
||||||
|
// Verify the palette Highlight is distinguishable from Window background
|
||||||
|
// This is the root cause of broken hover: if they're the same, hover is invisible
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto themes = tm.themes();
|
||||||
|
for (const auto& theme : themes) {
|
||||||
|
QVERIFY2(theme.selection != theme.background,
|
||||||
|
qPrintable(QString("Theme '%1': selection == background (%2)")
|
||||||
|
.arg(theme.name, theme.background.name())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void paletteHighlightIsSelection() {
|
||||||
|
// After applying theme, QPalette::Highlight must be theme.selection (not theme.hover)
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto& theme = tm.current();
|
||||||
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
QPalette pal = qApp->palette();
|
||||||
|
QCOMPARE(pal.color(QPalette::Highlight), theme.selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void treePageSwitching() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||||
|
QVERIFY(tree && pages);
|
||||||
|
|
||||||
|
// General is selected by default -> page 0
|
||||||
|
QCOMPARE(pages->currentIndex(), 0);
|
||||||
|
|
||||||
|
// Find "AI Features" item and select it
|
||||||
|
auto* envItem = tree->topLevelItem(0);
|
||||||
|
QVERIFY(envItem);
|
||||||
|
QTreeWidgetItem* aiItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
if (envItem->child(i)->text(0) == "AI Features") {
|
||||||
|
aiItem = envItem->child(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(aiItem);
|
||||||
|
tree->setCurrentItem(aiItem);
|
||||||
|
QCOMPARE(pages->currentIndex(), 1);
|
||||||
|
|
||||||
|
// Switch back to General
|
||||||
|
QTreeWidgetItem* generalItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
if (envItem->child(i)->text(0) == "General") {
|
||||||
|
generalItem = envItem->child(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(generalItem);
|
||||||
|
tree->setCurrentItem(generalItem);
|
||||||
|
QCOMPARE(pages->currentIndex(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void searchFilterHidesItems() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
auto* search = dlg.findChild<QLineEdit*>();
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
QVERIFY(search && tree);
|
||||||
|
|
||||||
|
auto* envItem = tree->topLevelItem(0);
|
||||||
|
QVERIFY(envItem);
|
||||||
|
|
||||||
|
// All children visible initially
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i)
|
||||||
|
QVERIFY(!envItem->child(i)->isHidden());
|
||||||
|
|
||||||
|
// Search for "MCP" - should hide General, show AI Features
|
||||||
|
search->setText("MCP");
|
||||||
|
QTreeWidgetItem* generalItem = nullptr;
|
||||||
|
QTreeWidgetItem* aiItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
auto* child = envItem->child(i);
|
||||||
|
if (child->text(0) == "General") generalItem = child;
|
||||||
|
if (child->text(0) == "AI Features") aiItem = child;
|
||||||
|
}
|
||||||
|
QVERIFY(generalItem && aiItem);
|
||||||
|
QVERIFY(generalItem->isHidden());
|
||||||
|
QVERIFY(!aiItem->isHidden());
|
||||||
|
|
||||||
|
// Clear search - all visible again
|
||||||
|
search->setText("");
|
||||||
|
QVERIFY(!generalItem->isHidden());
|
||||||
|
QVERIFY(!aiItem->isHidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshRateSpinBoxExists() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
defaults.refreshMs = 660;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||||
|
QVERIFY(spin);
|
||||||
|
QCOMPARE(spin->value(), 660);
|
||||||
|
QCOMPARE(spin->minimum(), 1);
|
||||||
|
QCOMPARE(spin->maximum(), 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshRateResultReflectsInput() {
|
||||||
|
OptionsResult input;
|
||||||
|
input.refreshMs = 200;
|
||||||
|
OptionsDialog dlg(input);
|
||||||
|
|
||||||
|
auto r = dlg.result();
|
||||||
|
QCOMPARE(r.refreshMs, 200);
|
||||||
|
|
||||||
|
// Change via spin box
|
||||||
|
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||||
|
QVERIFY(spin);
|
||||||
|
spin->setValue(100);
|
||||||
|
r = dlg.result();
|
||||||
|
QCOMPARE(r.refreshMs, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshRateClampsMin() {
|
||||||
|
OptionsResult input;
|
||||||
|
input.refreshMs = 0; // below minimum
|
||||||
|
OptionsDialog dlg(input);
|
||||||
|
|
||||||
|
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||||
|
QVERIFY(spin);
|
||||||
|
// QSpinBox clamps to minimum
|
||||||
|
QCOMPARE(spin->value(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogInheritsPalette() {
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto& theme = tm.current();
|
||||||
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
dlg.show();
|
||||||
|
QTest::qWaitForWindowExposed(&dlg);
|
||||||
|
|
||||||
|
// Dialog's effective palette should match the app palette
|
||||||
|
QPalette dlgPal = dlg.palette();
|
||||||
|
QPalette appPal = qApp->palette();
|
||||||
|
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Window), appPal.color(QPalette::Window));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::WindowText), appPal.color(QPalette::WindowText));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Highlight), appPal.color(QPalette::Highlight));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Button), appPal.color(QPalette::Button));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::ButtonText), appPal.color(QPalette::ButtonText));
|
||||||
|
|
||||||
|
// Highlight must be visible against background
|
||||||
|
QVERIFY(dlgPal.color(QPalette::Highlight) != dlgPal.color(QPalette::Window));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_MAIN(TestOptionsDialog)
|
||||||
|
#include "test_options_dialog.moc"
|
||||||
@@ -11,31 +11,37 @@ class TestTheme : public QObject {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private slots:
|
private slots:
|
||||||
void builtInThemes() {
|
void builtInThemes() {
|
||||||
Theme dark = Theme::reclassDark();
|
auto& tm = ThemeManager::instance();
|
||||||
QCOMPARE(dark.name, "Reclass Dark");
|
auto all = tm.themes();
|
||||||
QVERIFY(dark.background.isValid());
|
QVERIFY(all.size() >= 2);
|
||||||
QVERIFY(dark.text.isValid());
|
|
||||||
QVERIFY(dark.syntaxKeyword.isValid());
|
|
||||||
QVERIFY(dark.markerError.isValid());
|
|
||||||
|
|
||||||
Theme warm = Theme::warm();
|
// Find themes by name
|
||||||
QCOMPARE(warm.name, "Warm");
|
const Theme* dark = nullptr;
|
||||||
QVERIFY(warm.background.isValid());
|
const Theme* warm = nullptr;
|
||||||
QVERIFY(warm.text.isValid());
|
for (const auto& t : all) {
|
||||||
QCOMPARE(warm.background, QColor("#212121"));
|
if (t.name == "Reclass Dark") dark = &t;
|
||||||
QCOMPARE(warm.selection, QColor("#21213A"));
|
if (t.name == "Warm") warm = &t;
|
||||||
QCOMPARE(warm.syntaxKeyword, QColor("#AA9565"));
|
|
||||||
QCOMPARE(warm.syntaxType, QColor("#6B959F"));
|
|
||||||
}
|
}
|
||||||
|
QVERIFY(dark);
|
||||||
|
QCOMPARE(dark->name, QString("Reclass Dark"));
|
||||||
|
QVERIFY(dark->background.isValid());
|
||||||
|
QVERIFY(dark->text.isValid());
|
||||||
|
QVERIFY(dark->syntaxKeyword.isValid());
|
||||||
|
QVERIFY(dark->markerError.isValid());
|
||||||
|
|
||||||
void selectionColorFixed() {
|
QVERIFY(warm);
|
||||||
Theme dark = Theme::reclassDark();
|
QCOMPARE(warm->name, QString("Warm"));
|
||||||
QCOMPARE(dark.selection, QColor("#2b2b2b"));
|
QVERIFY(warm->background.isValid());
|
||||||
QVERIFY(dark.selection != QColor("#264f78"));
|
QVERIFY(warm->text.isValid());
|
||||||
|
QCOMPARE(warm->background, QColor("#212121"));
|
||||||
|
QCOMPARE(warm->selection, QColor("#21213A"));
|
||||||
|
QCOMPARE(warm->syntaxKeyword, QColor("#AA9565"));
|
||||||
|
QCOMPARE(warm->syntaxType, QColor("#6B959F"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void jsonRoundTrip() {
|
void jsonRoundTrip() {
|
||||||
Theme orig = Theme::reclassDark();
|
auto& tm = ThemeManager::instance();
|
||||||
|
Theme orig = tm.themes()[0];
|
||||||
QJsonObject json = orig.toJson();
|
QJsonObject json = orig.toJson();
|
||||||
Theme loaded = Theme::fromJson(json);
|
Theme loaded = Theme::fromJson(json);
|
||||||
|
|
||||||
@@ -54,7 +60,12 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void jsonRoundTripWarm() {
|
void jsonRoundTripWarm() {
|
||||||
Theme orig = Theme::warm();
|
auto& tm = ThemeManager::instance();
|
||||||
|
auto all = tm.themes();
|
||||||
|
Theme orig;
|
||||||
|
for (const auto& t : all)
|
||||||
|
if (t.name == "Warm") { orig = t; break; }
|
||||||
|
|
||||||
QJsonObject json = orig.toJson();
|
QJsonObject json = orig.toJson();
|
||||||
Theme loaded = Theme::fromJson(json);
|
Theme loaded = Theme::fromJson(json);
|
||||||
|
|
||||||
@@ -70,21 +81,27 @@ private slots:
|
|||||||
sparse["background"] = "#ff0000";
|
sparse["background"] = "#ff0000";
|
||||||
Theme t = Theme::fromJson(sparse);
|
Theme t = Theme::fromJson(sparse);
|
||||||
|
|
||||||
QCOMPARE(t.name, "Sparse");
|
QCOMPARE(t.name, QString("Sparse"));
|
||||||
QCOMPARE(t.background, QColor("#ff0000"));
|
QCOMPARE(t.background, QColor("#ff0000"));
|
||||||
// Missing fields fall back to reclassDark defaults
|
// Missing fields are default (invalid) QColor
|
||||||
Theme defaults = Theme::reclassDark();
|
QVERIFY(!t.text.isValid());
|
||||||
QCOMPARE(t.text, defaults.text);
|
QVERIFY(!t.syntaxKeyword.isValid());
|
||||||
QCOMPARE(t.syntaxKeyword, defaults.syntaxKeyword);
|
QVERIFY(!t.markerError.isValid());
|
||||||
QCOMPARE(t.markerError, defaults.markerError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void themeManagerHasBuiltIns() {
|
void themeManagerHasBuiltIns() {
|
||||||
auto& tm = ThemeManager::instance();
|
auto& tm = ThemeManager::instance();
|
||||||
auto all = tm.themes();
|
auto all = tm.themes();
|
||||||
QVERIFY(all.size() >= 2);
|
QVERIFY(all.size() >= 3);
|
||||||
QCOMPARE(all[0].name, "Reclass Dark");
|
QCOMPARE(all[0].name, QString("Reclass Dark"));
|
||||||
QCOMPARE(all[1].name, "Warm");
|
// VS2022 Dark and Warm are also loaded (order depends on filename sort)
|
||||||
|
bool hasVs = false, hasWarm = false;
|
||||||
|
for (const auto& t : all) {
|
||||||
|
if (t.name == "VS2022 Dark") hasVs = true;
|
||||||
|
if (t.name == "Warm") hasWarm = true;
|
||||||
|
}
|
||||||
|
QVERIFY(hasVs);
|
||||||
|
QVERIFY(hasWarm);
|
||||||
}
|
}
|
||||||
|
|
||||||
void themeManagerSwitch() {
|
void themeManagerSwitch() {
|
||||||
@@ -108,12 +125,12 @@ private slots:
|
|||||||
int initialCount = tm.themes().size();
|
int initialCount = tm.themes().size();
|
||||||
|
|
||||||
// Add
|
// Add
|
||||||
Theme custom = Theme::reclassDark();
|
Theme custom = tm.themes()[0];
|
||||||
custom.name = "Test Custom";
|
custom.name = "Test Custom";
|
||||||
custom.background = QColor("#ff0000");
|
custom.background = QColor("#ff0000");
|
||||||
tm.addTheme(custom);
|
tm.addTheme(custom);
|
||||||
QCOMPARE(tm.themes().size(), initialCount + 1);
|
QCOMPARE(tm.themes().size(), initialCount + 1);
|
||||||
QCOMPARE(tm.themes().last().name, "Test Custom");
|
QCOMPARE(tm.themes().last().name, QString("Test Custom"));
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
int idx = tm.themes().size() - 1;
|
int idx = tm.themes().size() - 1;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QListView>
|
#include <QListView>
|
||||||
#include <QStringListModel>
|
#include <QStringListModel>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QFrame>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "typeselectorpopup.h"
|
#include "typeselectorpopup.h"
|
||||||
@@ -198,6 +200,127 @@ private slots:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Isolate first-show cost with different window flags ──
|
||||||
|
|
||||||
|
void benchmarkFirstShow() {
|
||||||
|
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
|
||||||
|
|
||||||
|
struct FlagTest {
|
||||||
|
const char* name;
|
||||||
|
Qt::WindowFlags flags;
|
||||||
|
};
|
||||||
|
FlagTest tests[] = {
|
||||||
|
{"Qt::Popup|Frameless", Qt::Popup | Qt::FramelessWindowHint},
|
||||||
|
{"Qt::Tool|Frameless", Qt::Tool | Qt::FramelessWindowHint},
|
||||||
|
{"Qt::ToolTip", Qt::ToolTip},
|
||||||
|
{"Qt::Window|Frameless", Qt::Window | Qt::FramelessWindowHint},
|
||||||
|
{"Qt::Popup|Frameless (2nd)", Qt::Popup | Qt::FramelessWindowHint},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& test : tests) {
|
||||||
|
auto* f = new QFrame(nullptr, test.flags);
|
||||||
|
f->resize(300, 400);
|
||||||
|
|
||||||
|
QElapsedTimer t; t.start();
|
||||||
|
f->show();
|
||||||
|
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t2 = t.nsecsElapsed();
|
||||||
|
f->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
t.restart();
|
||||||
|
f->show();
|
||||||
|
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t4 = t.nsecsElapsed();
|
||||||
|
f->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== %1 ===").arg(test.name);
|
||||||
|
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||||
|
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeSelectorPopup: cold vs after warmUp
|
||||||
|
{
|
||||||
|
auto* popup = new TypeSelectorPopup();
|
||||||
|
TypeEntry dummy;
|
||||||
|
dummy.entryKind = TypeEntry::Primitive;
|
||||||
|
dummy.primitiveKind = NodeKind::Hex8;
|
||||||
|
dummy.displayName = "test";
|
||||||
|
popup->setTypes({dummy});
|
||||||
|
|
||||||
|
QElapsedTimer t; t.start();
|
||||||
|
popup->show();
|
||||||
|
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t2 = t.nsecsElapsed();
|
||||||
|
popup->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
t.restart();
|
||||||
|
popup->show();
|
||||||
|
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t4 = t.nsecsElapsed();
|
||||||
|
popup->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== TypeSelectorPopup (cold, Qt::Popup) ===");
|
||||||
|
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||||
|
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||||
|
delete popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean order test: dummy popup with children FIRST, then TypeSelectorPopup
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug() << "=== CLEAN: dummy popup first, then TypeSelectorPopup ===";
|
||||||
|
{
|
||||||
|
auto* dummy = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||||
|
dummy->resize(300, 400);
|
||||||
|
auto* dLay = new QVBoxLayout(dummy);
|
||||||
|
dLay->addWidget(new QLabel("dummy"));
|
||||||
|
dLay->addWidget(new QLineEdit);
|
||||||
|
auto* dModel = new QStringListModel(dummy);
|
||||||
|
QStringList dItems; for (int i = 0; i < 10; i++) dItems << "x";
|
||||||
|
dModel->setStringList(dItems);
|
||||||
|
auto* dLv = new QListView; dLv->setModel(dModel);
|
||||||
|
dLay->addWidget(dLv);
|
||||||
|
|
||||||
|
QElapsedTimer t; t.start();
|
||||||
|
dummy->show();
|
||||||
|
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t2 = t.nsecsElapsed();
|
||||||
|
dummy->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qDebug().noquote() << QString(" Dummy popup: show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||||
|
delete dummy;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto* popup = new TypeSelectorPopup();
|
||||||
|
TypeEntry e;
|
||||||
|
e.entryKind = TypeEntry::Primitive;
|
||||||
|
e.primitiveKind = NodeKind::Hex8;
|
||||||
|
e.displayName = "test";
|
||||||
|
popup->setTypes({e});
|
||||||
|
popup->resize(300, 400);
|
||||||
|
QElapsedTimer t; t.start();
|
||||||
|
popup->show();
|
||||||
|
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 t2 = t.nsecsElapsed();
|
||||||
|
popup->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
qDebug().noquote() << QString(" TypeSelectorPopup (after dummy): show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||||
|
delete popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Popup data model ──
|
// ── Popup data model ──
|
||||||
|
|
||||||
void testPopupListsRootStructs() {
|
void testPopupListsRootStructs() {
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ static void buildValidationTree(NodeTree& tree) {
|
|||||||
field(46, NodeKind::Hex32, "field_h32");
|
field(46, NodeKind::Hex32, "field_h32");
|
||||||
field(50, NodeKind::Hex64, "field_h64");
|
field(50, NodeKind::Hex64, "field_h64");
|
||||||
field(58, NodeKind::Pointer64, "field_ptr");
|
field(58, NodeKind::Pointer64, "field_ptr");
|
||||||
field(66, NodeKind::Padding, "pad0");
|
field(66, NodeKind::Hex32, "pad0");
|
||||||
tree.nodes.last().arrayLen = 6;
|
field(70, NodeKind::Hex16, "pad1");
|
||||||
fieldArr(72, NodeKind::UInt32, 4, "field_arr");
|
fieldArr(72, NodeKind::UInt32, 4, "field_arr");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,9 +725,9 @@ private slots:
|
|||||||
QCOMPARE(m_doc->undoStack.count(), 0);
|
QCOMPARE(m_doc->undoStack.count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── changeNodeKind size transitions: shrink inserts padding ──
|
// ── changeNodeKind size transitions: shrink inserts hex nodes ──
|
||||||
|
|
||||||
void testChangeKindShrinkInsertsPadding() {
|
void testChangeKindShrinkInsertsHexNodes() {
|
||||||
int idx = findNode(m_doc->tree, "field_u32");
|
int idx = findNode(m_doc->tree, "field_u32");
|
||||||
QVERIFY(idx >= 0);
|
QVERIFY(idx >= 0);
|
||||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes
|
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes
|
||||||
@@ -737,7 +737,7 @@ private slots:
|
|||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8);
|
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8);
|
||||||
// Should have inserted padding nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
// Should have inserted hex nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
||||||
QVERIFY(m_doc->tree.nodes.size() > origCount);
|
QVERIFY(m_doc->tree.nodes.size() > origCount);
|
||||||
|
|
||||||
// Undo restores everything
|
// Undo restores everything
|
||||||
@@ -985,37 +985,6 @@ private slots:
|
|||||||
QVERIFY(!m_editor->isEditing());
|
QVERIFY(!m_editor->isEditing());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Editor: padding value edit blocked, name/type still work ──
|
|
||||||
|
|
||||||
void testPaddingEditRestrictions() {
|
|
||||||
m_ctrl->refresh();
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
ComposeResult result = m_doc->compose();
|
|
||||||
m_editor->applyDocument(result);
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// Find padding line
|
|
||||||
int padLine = -1;
|
|
||||||
for (int i = 0; i < result.meta.size(); i++) {
|
|
||||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
|
||||||
result.meta[i].lineKind == LineKind::Field) {
|
|
||||||
padLine = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY(padLine >= 0);
|
|
||||||
|
|
||||||
// Value edit rejected
|
|
||||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, padLine));
|
|
||||||
|
|
||||||
// Type edit accepted
|
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, padLine);
|
|
||||||
QVERIFY(ok);
|
|
||||||
m_editor->cancelInlineEdit();
|
|
||||||
QApplication::processEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Editor: struct header rejects value edit ──
|
// ── Editor: struct header rejects value edit ──
|
||||||
|
|
||||||
void testStructHeaderRejectsValueEdit() {
|
void testStructHeaderRejectsValueEdit() {
|
||||||
|
|||||||
463
tests/test_windbg_provider.cpp
Normal file
463
tests/test_windbg_provider.cpp
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
#include <QTest>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "providers/provider.h"
|
||||||
|
#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <tlhelp32.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <dbgeng.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
static const char* CDB_PATH = "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\cdb.exe";
|
||||||
|
static const int DBG_PORT = 5055;
|
||||||
|
|
||||||
|
class TestWinDbgProvider : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private:
|
||||||
|
QProcess* m_cdbProcess = nullptr;
|
||||||
|
uint32_t m_notepadPid = 0;
|
||||||
|
bool m_weSpawnedNotepad = false;
|
||||||
|
QString m_connString;
|
||||||
|
|
||||||
|
static uint32_t findProcess(const wchar_t* name)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
if (snap == INVALID_HANDLE_VALUE) return 0;
|
||||||
|
PROCESSENTRY32W entry;
|
||||||
|
entry.dwSize = sizeof(entry);
|
||||||
|
uint32_t pid = 0;
|
||||||
|
if (Process32FirstW(snap, &entry)) {
|
||||||
|
do {
|
||||||
|
if (_wcsicmp(entry.szExeFile, name) == 0) {
|
||||||
|
pid = entry.th32ProcessID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (Process32NextW(snap, &entry));
|
||||||
|
}
|
||||||
|
CloseHandle(snap);
|
||||||
|
return pid;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(name); return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t launchNotepad()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
STARTUPINFOW si{};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
if (CreateProcessW(L"C:\\Windows\\notepad.exe", nullptr, nullptr, nullptr,
|
||||||
|
FALSE, 0, nullptr, nullptr, &si, &pi)) {
|
||||||
|
WaitForInputIdle(pi.hProcess, 3000);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
return pi.dwProcessId;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void terminateProcess(uint32_t pid)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
|
||||||
|
if (h) { TerminateProcess(h, 0); CloseHandle(h); }
|
||||||
|
#else
|
||||||
|
Q_UNUSED(pid);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
|
||||||
|
// ── Fixture ──
|
||||||
|
|
||||||
|
/// Try a quick DebugConnect to see if the port is already serving.
|
||||||
|
static bool canConnect(const QString& connStr)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
IDebugClient* probe = nullptr;
|
||||||
|
QByteArray utf8 = connStr.toUtf8();
|
||||||
|
HRESULT hr = DebugConnect(utf8.constData(), IID_IDebugClient, (void**)&probe);
|
||||||
|
if (SUCCEEDED(hr) && probe) {
|
||||||
|
probe->EndSession(DEBUG_END_DISCONNECT);
|
||||||
|
probe->Release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
Q_UNUSED(connStr);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
m_connString = QString("tcp:Port=%1,Server=localhost").arg(DBG_PORT);
|
||||||
|
|
||||||
|
// If a debug server is already listening (e.g. WinDbg with .server),
|
||||||
|
// skip launching our own cdb.exe.
|
||||||
|
if (canConnect(m_connString)) {
|
||||||
|
qDebug() << "Debug server already running on port" << DBG_PORT << "— using it";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No server running — launch cdb ourselves
|
||||||
|
m_notepadPid = findProcess(L"notepad.exe");
|
||||||
|
if (m_notepadPid == 0) {
|
||||||
|
m_notepadPid = launchNotepad();
|
||||||
|
m_weSpawnedNotepad = true;
|
||||||
|
}
|
||||||
|
QVERIFY2(m_notepadPid != 0, "Need notepad.exe running");
|
||||||
|
qDebug() << "Using notepad.exe PID:" << m_notepadPid;
|
||||||
|
|
||||||
|
m_cdbProcess = new QProcess(this);
|
||||||
|
QStringList args;
|
||||||
|
args << "-server" << QString("tcp:port=%1").arg(DBG_PORT)
|
||||||
|
<< "-pv"
|
||||||
|
<< "-p" << QString::number(m_notepadPid);
|
||||||
|
|
||||||
|
m_cdbProcess->setProgram(CDB_PATH);
|
||||||
|
m_cdbProcess->setArguments(args);
|
||||||
|
m_cdbProcess->start();
|
||||||
|
|
||||||
|
QVERIFY2(m_cdbProcess->waitForStarted(5000), "Failed to start cdb.exe");
|
||||||
|
QThread::sleep(3);
|
||||||
|
|
||||||
|
qDebug() << "cdb.exe debug server started on port" << DBG_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanupTestCase()
|
||||||
|
{
|
||||||
|
if (m_cdbProcess) {
|
||||||
|
m_cdbProcess->write("q\n");
|
||||||
|
if (!m_cdbProcess->waitForFinished(5000))
|
||||||
|
m_cdbProcess->kill();
|
||||||
|
delete m_cdbProcess;
|
||||||
|
m_cdbProcess = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_weSpawnedNotepad && m_notepadPid)
|
||||||
|
terminateProcess(m_notepadPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Plugin metadata ──
|
||||||
|
|
||||||
|
void plugin_name()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QCOMPARE(plugin.Name(), std::string("WinDbg Memory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_version()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QCOMPARE(plugin.Version(), std::string("2.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_canHandle_tcp()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QVERIFY(plugin.canHandle("tcp:Port=5055,Server=localhost"));
|
||||||
|
QVERIFY(plugin.canHandle("TCP:Port=1234,Server=10.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_canHandle_npipe()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QVERIFY(plugin.canHandle("npipe:Pipe=test,Server=localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_canHandle_pid()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QVERIFY(plugin.canHandle("pid:1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_canHandle_dump()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QVERIFY(plugin.canHandle("dump:C:/test.dmp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_canHandle_invalid()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QVERIFY(!plugin.canHandle(""));
|
||||||
|
QVERIFY(!plugin.canHandle("1234"));
|
||||||
|
QVERIFY(!plugin.canHandle("file:///test.bin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connection failure ──
|
||||||
|
|
||||||
|
void provider_connect_badPort()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov("tcp:Port=59999,Server=localhost");
|
||||||
|
QVERIFY(!prov.isValid());
|
||||||
|
QCOMPARE(prov.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_connect_badPipe()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov("npipe:Pipe=nonexistent_reclass_test_pipe,Server=localhost");
|
||||||
|
QVERIFY(!prov.isValid());
|
||||||
|
QCOMPARE(prov.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void plugin_createProvider_badConnection()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QString error;
|
||||||
|
auto prov = plugin.createProvider("tcp:Port=59999,Server=localhost", &error);
|
||||||
|
QVERIFY(prov == nullptr);
|
||||||
|
QVERIFY(!error.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connect and read (main thread) ──
|
||||||
|
|
||||||
|
void provider_connect_valid()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY2(prov.isValid(), "Should connect to cdb debug server");
|
||||||
|
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||||
|
QVERIFY(prov.size() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_name()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
QVERIFY(!prov.name().isEmpty());
|
||||||
|
qDebug() << "Provider name:" << prov.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_isLive()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
QVERIFY(prov.isLive());
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_baseAddress()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module");
|
||||||
|
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_setBase()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
uint64_t orig = prov.base();
|
||||||
|
prov.setBase(0x1000);
|
||||||
|
QCOMPARE(prov.base(), (uint64_t)0x1000);
|
||||||
|
prov.setBase(orig);
|
||||||
|
QCOMPARE(prov.base(), orig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read: MZ header on main thread ──
|
||||||
|
|
||||||
|
void provider_read_mz_mainThread()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
uint8_t buf[2] = {};
|
||||||
|
bool ok = prov.read(0, buf, 2);
|
||||||
|
QVERIFY2(ok, "Failed to read from debug session (main thread)");
|
||||||
|
QCOMPARE(buf[0], (uint8_t)'M');
|
||||||
|
QCOMPARE(buf[1], (uint8_t)'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read: MZ header from a background thread (the actual failure case) ──
|
||||||
|
|
||||||
|
void provider_read_mz_backgroundThread()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
// Simulate what the controller's refresh does:
|
||||||
|
// read from a QtConcurrent worker thread.
|
||||||
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
||||||
|
return prov.readBytes(0, 128);
|
||||||
|
});
|
||||||
|
future.waitForFinished();
|
||||||
|
QByteArray data = future.result();
|
||||||
|
|
||||||
|
QCOMPARE(data.size(), 128);
|
||||||
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
||||||
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read: bulk data from background thread ──
|
||||||
|
|
||||||
|
void provider_read_4k_backgroundThread()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
||||||
|
return prov.readBytes(0, 4096);
|
||||||
|
});
|
||||||
|
future.waitForFinished();
|
||||||
|
QByteArray data = future.result();
|
||||||
|
|
||||||
|
QCOMPARE(data.size(), 4096);
|
||||||
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
||||||
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
||||||
|
|
||||||
|
// Verify it's not all zeros (the old failure mode)
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < data.size(); ++i) {
|
||||||
|
if (data[i] != '\0') { allZero = false; break; }
|
||||||
|
}
|
||||||
|
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple sequential background reads (simulates refresh timer) ──
|
||||||
|
|
||||||
|
void provider_read_multipleRefreshes()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
||||||
|
return prov.readBytes(0, 128);
|
||||||
|
});
|
||||||
|
future.waitForFinished();
|
||||||
|
QByteArray data = future.result();
|
||||||
|
QCOMPARE(data.size(), 128);
|
||||||
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
||||||
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read helpers ──
|
||||||
|
|
||||||
|
void provider_readU16()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
QCOMPARE(prov.readU16(0), (uint16_t)0x5A4D); // "MZ" little-endian
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_read_peSignature()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
uint32_t peOffset = prov.readU32(0x3C);
|
||||||
|
QVERIFY2(peOffset > 0 && peOffset < 0x1000, "PE offset should be reasonable");
|
||||||
|
|
||||||
|
uint8_t sig[4] = {};
|
||||||
|
bool ok = prov.read(peOffset, sig, 4);
|
||||||
|
QVERIFY(ok);
|
||||||
|
QCOMPARE(sig[0], (uint8_t)'P');
|
||||||
|
QCOMPARE(sig[1], (uint8_t)'E');
|
||||||
|
QCOMPARE(sig[2], (uint8_t)0);
|
||||||
|
QCOMPARE(sig[3], (uint8_t)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Edge cases ──
|
||||||
|
|
||||||
|
void provider_read_zeroLength()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
uint8_t buf = 0xFF;
|
||||||
|
QVERIFY(!prov.read(0, &buf, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_read_negativeLength()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
uint8_t buf = 0xFF;
|
||||||
|
QVERIFY(!prov.read(0, &buf, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── getSymbol ──
|
||||||
|
|
||||||
|
void provider_getSymbol()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
QString sym = prov.getSymbol(0);
|
||||||
|
qDebug() << "Symbol at base+0:" << sym;
|
||||||
|
// Should not crash; may or may not resolve
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_getSymbol_backgroundThread()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
QFuture<QString> future = QtConcurrent::run([&prov]() -> QString {
|
||||||
|
return prov.getSymbol(0);
|
||||||
|
});
|
||||||
|
future.waitForFinished();
|
||||||
|
// Should not crash from background thread
|
||||||
|
qDebug() << "Symbol (bg thread):" << future.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── createProvider full flow ──
|
||||||
|
|
||||||
|
void plugin_createProvider_valid()
|
||||||
|
{
|
||||||
|
WinDbgMemoryPlugin plugin;
|
||||||
|
QString error;
|
||||||
|
auto prov = plugin.createProvider(m_connString, &error);
|
||||||
|
QVERIFY2(prov != nullptr, qPrintable("createProvider failed: " + error));
|
||||||
|
QVERIFY(prov->isValid());
|
||||||
|
|
||||||
|
uint8_t mz[2] = {};
|
||||||
|
QVERIFY(prov->read(0, mz, 2));
|
||||||
|
QCOMPARE(mz[0], (uint8_t)'M');
|
||||||
|
QCOMPARE(mz[1], (uint8_t)'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple concurrent connections ──
|
||||||
|
|
||||||
|
void provider_multipleConcurrent()
|
||||||
|
{
|
||||||
|
WinDbgMemoryProvider prov1(m_connString);
|
||||||
|
WinDbgMemoryProvider prov2(m_connString);
|
||||||
|
|
||||||
|
QVERIFY(prov1.isValid());
|
||||||
|
QVERIFY(prov2.isValid());
|
||||||
|
|
||||||
|
QCOMPARE(prov1.readU16(0), (uint16_t)0x5A4D);
|
||||||
|
QCOMPARE(prov2.readU16(0), (uint16_t)0x5A4D);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Factory ──
|
||||||
|
|
||||||
|
void factory_createPlugin()
|
||||||
|
{
|
||||||
|
IPlugin* raw = CreatePlugin();
|
||||||
|
QVERIFY(raw != nullptr);
|
||||||
|
QCOMPARE(raw->Type(), IPlugin::ProviderPlugin);
|
||||||
|
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
||||||
|
delete raw;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_MAIN(TestWinDbgProvider)
|
||||||
|
#include "test_windbg_provider.moc"
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <io.h>
|
#include <io.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/select.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user