#include "RcNetCompatPlugin.h" #include "RcNetCompatProvider.h" #include "../../src/processpicker.h" #include #include #include #include #include #include #include #include // -- 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(path.utf16())); if (!hMod) hMod = LoadLibraryExW(reinterpret_cast(path.utf16()), nullptr, DONT_RESOLVE_DLL_REFERENCES); if (!hMod) return false; auto* dos = reinterpret_cast(hMod); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; auto* nt = reinterpret_cast( reinterpret_cast(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(&nt->OptionalHeader); if (opt->NumberOfRvaAndSizes > kClrIndex) { rva = opt->DataDirectory[kClrIndex].VirtualAddress; dirSize = opt->DataDirectory[kClrIndex].Size; } } else { auto* opt = reinterpret_cast(&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(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(m_lib->resolve("EnumerateProcesses")); m_fns.OpenRemoteProcess = reinterpret_cast(m_lib->resolve("OpenRemoteProcess")); m_fns.IsProcessValid = reinterpret_cast(m_lib->resolve("IsProcessValid")); m_fns.CloseRemoteProcess = reinterpret_cast(m_lib->resolve("CloseRemoteProcess")); m_fns.ReadRemoteMemory = reinterpret_cast(m_lib->resolve("ReadRemoteMemory")); m_fns.WriteRemoteMemory = reinterpret_cast(m_lib->resolve("WriteRemoteMemory")); m_fns.EnumerateRemoteSectionsAndModules = reinterpret_cast( m_lib->resolve("EnumerateRemoteSectionsAndModules")); m_fns.ControlRemoteProcess = reinterpret_cast(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(); 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 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(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 pluginProcesses = enumerateProcesses(); QList 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* 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(data->Id); info.name = QString::fromUtf16(data->Name); info.path = QString::fromUtf16(data->Path); g_processCollector.dest->append(info); } } // anonymous namespace QVector RcNetCompatPlugin::enumerateProcesses() { QVector 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(); }