From fcb525df364333f886efb10a2fdd962d3da3bb13 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Mon, 2 Mar 2020 23:38:51 -0700 Subject: [PATCH] yeah...that was never a class. improve performance and add current process function --- GUI/host/host.h | 2 +- GUI/host/textthread.cpp | 2 +- GUI/host/textthread.h | 4 +- GUI/mainwindow.cpp | 1036 ++++++++++++++++++++------------------- GUI/mainwindow.h | 46 +- 5 files changed, 533 insertions(+), 557 deletions(-) diff --git a/GUI/host/host.h b/GUI/host/host.h index 1016b8f..033be04 100644 --- a/GUI/host/host.h +++ b/GUI/host/host.h @@ -5,7 +5,7 @@ namespace Host { - using ProcessEventHandler = std::function; + using ProcessEventHandler = void(*)(DWORD); using ThreadEventHandler = std::function; using HookEventHandler = std::function; void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output); diff --git a/GUI/host/textthread.cpp b/GUI/host/textthread.cpp index b08556e..2628151 100644 --- a/GUI/host/textthread.cpp +++ b/GUI/host/textthread.cpp @@ -77,7 +77,7 @@ void TextThread::Flush() if (storage->size() > maxHistorySize) storage->erase(0, storage->size() - maxHistorySize); // https://github.com/Artikash/Textractor/issues/127#issuecomment-486882983 } - std::deque sentences; + std::vector sentences; queuedSentences->swap(sentences); int totalSize = 0; for (auto& sentence : sentences) diff --git a/GUI/host/textthread.h b/GUI/host/textthread.h index e244a7d..3208846 100644 --- a/GUI/host/textthread.h +++ b/GUI/host/textthread.h @@ -6,7 +6,7 @@ class TextThread { public: - using OutputCallback = std::function; + using OutputCallback = bool(*)(TextThread&, std::wstring&); inline static OutputCallback Output; inline static bool filterRepetition = true; @@ -37,7 +37,7 @@ private: std::unordered_set repeatingChars; std::mutex bufferMutex; DWORD lastPushTime = 0; - Synchronized> queuedSentences; + Synchronized> queuedSentences; struct TimerDeleter { void operator()(HANDLE h) { DeleteTimerQueueTimer(NULL, h, INVALID_HANDLE_VALUE); } }; AutoHandle timer = NULL; }; diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index c6df9ed..4de10c5 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -2,6 +2,8 @@ #include "ui_mainwindow.h" #include "defs.h" #include "module.h" +#include "extenwindow.h" +#include "host/host.h" #include "host/hookcode.h" #include #include @@ -62,38 +64,538 @@ extern const wchar_t* CL_OPTIONS; extern const wchar_t* LAUNCH_FAILED; extern const wchar_t* INVALID_CODE; -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent), - ui(new Ui::MainWindow), - extenWindow(new ExtenWindow(this)) +namespace { - ui->setupUi(this); - for (auto [text, slot] : Array{ - { ATTACH, &MainWindow::AttachProcess }, - { LAUNCH, &MainWindow::LaunchProcess }, - { DETACH, &MainWindow::DetachProcess }, - { FORGET, &MainWindow::ForgetProcess }, - { ADD_HOOK, &MainWindow::AddHook }, - { REMOVE_HOOKS, &MainWindow::RemoveHooks }, - { SAVE_HOOKS, &MainWindow::SaveHooks }, - { SEARCH_FOR_HOOKS, &MainWindow::FindHooks }, - { SETTINGS, &MainWindow::Settings }, - { EXTENSIONS, &MainWindow::Extensions } + constexpr auto HOOK_SAVE_FILE = u8"SavedHooks.txt"; + constexpr auto GAME_SAVE_FILE = u8"SavedGames.txt"; + + std::atomic selectedProcessId = 0; + Ui::MainWindow ui{}; + ExtenWindow* extenWindow = nullptr; + std::unordered_set alreadyAttached; + bool autoAttach = false, autoAttachSavedOnly = true; + bool showSystemProcesses = false; + uint64_t savedThreadCtx = 0, savedThreadCtx2 = 0; + wchar_t savedThreadCode[1000] = {}; + TextThread* current = nullptr; + MainWindow* This = nullptr; + + QString TextThreadString(TextThread& thread) + { + return QString("%1:%2:%3:%4:%5: %6").arg( + QString::number(thread.handle, 16), + QString::number(thread.tp.processId, 16), + QString::number(thread.tp.addr, 16), + QString::number(thread.tp.ctx, 16), + QString::number(thread.tp.ctx2, 16) + ).toUpper().arg(S(thread.name)); + } + + ThreadParam ParseTextThreadString(QString ttString) + { + QStringList threadParam = ttString.split(":"); + return { threadParam[1].toUInt(nullptr, 16), threadParam[2].toULongLong(nullptr, 16), threadParam[3].toULongLong(nullptr, 16), threadParam[4].toULongLong(nullptr, 16) }; + } + + std::array GetSentenceInfo(TextThread& thread) + { + void (*AddSentence)(int64_t, const wchar_t*) = [](int64_t number, const wchar_t* sentence) + { + // pointer from Host::GetThread may not stay valid unless on main thread + QMetaObject::invokeMethod(This, [number, sentence = std::wstring(sentence)]{ if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(sentence); }); + }; + DWORD (*GetSelectedProcessId)() = [] { return selectedProcessId.load(); }; + + return + { { + { "current select", &thread == current }, + { "text number", thread.handle }, + { "process id", thread.tp.processId }, + { "hook address", (int64_t)thread.tp.addr }, + { "text handle", thread.handle }, + { "text name", (int64_t)thread.name.c_str() }, + { "void (*AddSentence)(int64_t number, const wchar_t* sentence)", (int64_t)AddSentence }, + { "DWORD (*GetSelectedProcessId)()", (int64_t)GetSelectedProcessId }, + { nullptr, 0 } // nullptr marks end of info array + } }; + } + + std::optional UserSelectedProcess() + { + QStringList savedProcesses = QString::fromUtf8(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); + std::reverse(savedProcesses.begin(), savedProcesses.end()); + savedProcesses.removeDuplicates(); + savedProcesses.insert(1, FROM_COMPUTER); + QString process = QInputDialog::getItem(This, SELECT_PROCESS, SELECT_PROCESS_INFO, savedProcesses, 0, true, &ok, Qt::WindowCloseButtonHint); + if (process == FROM_COMPUTER) process = QDir::toNativeSeparators(QFileDialog::getOpenFileName(This, SELECT_PROCESS, "C:\\", PROCESSES)); + if (ok && process.contains('\\')) return S(process); + return {}; + } + + void AttachProcess() + { + QMultiHash allProcesses; + for (auto [processId, processName] : GetAllProcesses()) + if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::wstring::npos)) + allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId); + + QStringList processList(allProcesses.uniqueKeys()); + processList.sort(Qt::CaseInsensitive); + if (QString process = QInputDialog::getItem(This, SELECT_PROCESS, ATTACH_INFO, processList, 0, true, &ok, Qt::WindowCloseButtonHint); ok) + if (process.toInt(nullptr, 0)) Host::InjectProcess(process.toInt(nullptr, 0)); + else for (auto processId : allProcesses.values(process)) Host::InjectProcess(processId); + } + + void LaunchProcess() + { + std::wstring process; + if (auto selected = UserSelectedProcess()) process = selected.value(); + else return; + std::wstring path = std::wstring(process).erase(process.rfind(L'\\')); + + PROCESS_INFORMATION info = {}; + if (!x64 && QMessageBox::question(This, SELECT_PROCESS, USE_JP_LOCALE) == QMessageBox::Yes) + { + if (HMODULE localeEmulator = LoadLibraryW(L"LoaderDll")) + { + // see https://github.com/xupefei/Locale-Emulator/blob/aa99dec3b25708e676c90acf5fed9beaac319160/LEProc/LoaderWrapper.cs#L252 + struct + { + ULONG AnsiCodePage = SHIFT_JIS; + ULONG OemCodePage = SHIFT_JIS; + ULONG LocaleID = LANG_JAPANESE; + ULONG DefaultCharset = SHIFTJIS_CHARSET; + ULONG HookUiLanguageApi = FALSE; + WCHAR DefaultFaceName[LF_FACESIZE] = {}; + TIME_ZONE_INFORMATION Timezone; + ULONG64 Unused = 0; + } LEB; + GetTimeZoneInformation(&LEB.Timezone); + ((LONG(__stdcall*)(decltype(&LEB), LPCWSTR appName, LPWSTR commandLine, LPCWSTR currentDir, void*, void*, PROCESS_INFORMATION*, void*, void*, void*, void*)) + GetProcAddress(localeEmulator, "LeCreateProcess"))(&LEB, process.c_str(), NULL, path.c_str(), NULL, NULL, &info, NULL, NULL, NULL, NULL); + } + } + if (info.hProcess == NULL) + { + STARTUPINFOW DUMMY = { sizeof(DUMMY) }; + CreateProcessW(process.c_str(), NULL, nullptr, nullptr, FALSE, 0, nullptr, path.c_str(), &DUMMY, &info); + } + if (info.hProcess == NULL) return Host::AddConsoleOutput(LAUNCH_FAILED); + Host::InjectProcess(info.dwProcessId); + CloseHandle(info.hProcess); + CloseHandle(info.hThread); + } + + void DetachProcess() + { + try { Host::DetachProcess(selectedProcessId); } + catch (std::out_of_range) {} + } + + void ForgetProcess() + { + std::optional processName = GetModuleFilename(selectedProcessId); + if (!processName) processName = UserSelectedProcess(); + DetachProcess(); + if (!processName) return; + for (auto file : { GAME_SAVE_FILE, HOOK_SAVE_FILE }) + { + QStringList lines = QString::fromUtf8(QTextFile(file, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); + lines.erase(std::remove_if(lines.begin(), lines.end(), [&](const QString& line) { return line.contains(S(processName.value())); }), lines.end()); + QTextFile(file, QIODevice::WriteOnly | QIODevice::Truncate).write(lines.join("\n").append("\n").toUtf8()); + } + } + + void FindHooks(); + + void AddHook(QString hook) + { + if (QString hookCode = QInputDialog::getText(This, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, hook, &ok, Qt::WindowCloseButtonHint); ok) + if (hookCode.startsWith("S") || hookCode.startsWith("/S")) FindHooks(); + else if (auto hp = HookCode::Parse(S(hookCode))) try { Host::InsertHook(selectedProcessId, hp.value()); } catch (std::out_of_range) {} + else Host::AddConsoleOutput(INVALID_CODE); + } + + void AddHook() + { + AddHook(""); + } + + void RemoveHooks() + { + DWORD processId = selectedProcessId; + std::unordered_map hooks; + for (int i = 0; i < ui.ttCombo->count(); ++i) + { + ThreadParam tp = ParseTextThreadString(ui.ttCombo->itemText(i)); + if (tp.processId == selectedProcessId) hooks[tp.addr] = Host::GetThread(tp).hp; + } + auto hookList = new QListWidget(This); + hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); + hookList->setAttribute(Qt::WA_DeleteOnClose); + hookList->setMinimumSize({ 300, 50 }); + hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK); + for (auto [address, hp] : hooks) + new QListWidgetItem(QString(hp.name) + "@" + QString::number(address, 16), hookList); + QObject::connect(hookList, &QListWidget::itemDoubleClicked, [processId, hookList](QListWidgetItem* item) + { + try + { + Host::RemoveHook(processId, item->text().split("@")[1].toULongLong(nullptr, 16)); + delete item; + } + catch (std::out_of_range) { hookList->close(); } + }); + hookList->show(); + } + + void SaveHooks() + { + auto processName = GetModuleFilename(selectedProcessId); + if (!processName) return; + QHash hookCodes; + for (int i = 0; i < ui.ttCombo->count(); ++i) + { + ThreadParam tp = ParseTextThreadString(ui.ttCombo->itemText(i)); + if (tp.processId == selectedProcessId) + { + HookParam hp = Host::GetThread(tp).hp; + if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(HookCode::Generate(hp, tp.processId)); + } + } + auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); + ThreadParam tp = current->tp; + if (tp.processId == selectedProcessId) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(HookCode::Generate(current->hp, tp.processId))); + QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); + } + + void FindHooks() + { + QMessageBox::information(This, SEARCH_FOR_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING); + + DWORD processId = selectedProcessId; + SearchParam sp = {}; + sp.codepage = Host::defaultCodepage; + bool searchForText = false, customSettings = false; + QRegularExpression filter(".", QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::DotMatchesEverythingOption); + + QDialog dialog(This, Qt::WindowCloseButtonHint); + QFormLayout layout(&dialog); + QCheckBox cjkCheckbox(&dialog); + layout.addRow(SEARCH_CJK, &cjkCheckbox); + QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help | QDialogButtonBox::Retry, &dialog); + layout.addRow(&confirm); + confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH); + confirm.button(QDialogButtonBox::Retry)->setText(SEARCH_FOR_TEXT); + confirm.button(QDialogButtonBox::Help)->setText(SETTINGS); + QObject::connect(&confirm, &QDialogButtonBox::clicked, [&](QAbstractButton* button) + { + if (button == confirm.button(QDialogButtonBox::Retry)) searchForText = true; + if (button == confirm.button(QDialogButtonBox::Help)) customSettings = true; + dialog.accept(); + }); + dialog.setWindowTitle(SEARCH_FOR_HOOKS); + if (!dialog.exec()) return; + + if (searchForText) + { + QDialog dialog(This, Qt::WindowCloseButtonHint); + QFormLayout layout(&dialog); + QLineEdit textInput(&dialog); + layout.addRow(TEXT, &textInput); + QSpinBox codepageInput(&dialog); + codepageInput.setMaximum(INT_MAX); + codepageInput.setValue(sp.codepage); + layout.addRow(CODEPAGE, &codepageInput); + QDialogButtonBox confirm(QDialogButtonBox::Ok); + QObject::connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + layout.addRow(&confirm); + if (!dialog.exec()) return; + wcsncpy_s(sp.text, S(textInput.text()).c_str(), PATTERN_SIZE - 1); + try { Host::FindHooks(selectedProcessId, sp); } + catch (std::out_of_range) {} + return; + } + + if (customSettings) + { + QDialog dialog(This, Qt::WindowCloseButtonHint); + QFormLayout layout(&dialog); + QLineEdit patternInput(x64 ? "CC CC 48 89" : "55 8B EC", &dialog); + assert(QByteArray::fromHex(patternInput.text().toUtf8()) == QByteArray((const char*)sp.pattern, sp.length)); + layout.addRow(SEARCH_PATTERN, &patternInput); + for (auto [value, label] : Array{ + { sp.searchTime, SEARCH_DURATION }, + { sp.offset, PATTERN_OFFSET }, + { sp.maxRecords, MAX_HOOK_SEARCH_RECORDS }, + { sp.codepage, CODEPAGE }, + }) + { + auto spinBox = new QSpinBox(&dialog); + spinBox->setMaximum(INT_MAX); + spinBox->setValue(value); + layout.addRow(label, spinBox); + QObject::connect(spinBox, qOverload(&QSpinBox::valueChanged), [&value](int newValue) { value = newValue; }); + } + QLineEdit boundInput(QFileInfo(S(GetModuleFilename(selectedProcessId).value_or(L""))).fileName(), &dialog); + layout.addRow(SEARCH_MODULE, &boundInput); + for (auto [value, label] : Array{ + { sp.minAddress, MIN_ADDRESS }, + { sp.maxAddress, MAX_ADDRESS }, + { sp.padding, STRING_OFFSET }, + }) + { + auto input = new QLineEdit(QString::number(value, 16), &dialog); + layout.addRow(label, input); + QObject::connect(input, &QLineEdit::textEdited, [&value](QString text) { if (uintptr_t newValue = text.toULongLong(&ok, 16); ok) value = newValue; }); + } + QLineEdit filterInput(filter.pattern(), &dialog); + layout.addRow(HOOK_SEARCH_FILTER, &filterInput); + QPushButton startButton(START_HOOK_SEARCH, &dialog); + layout.addWidget(&startButton); + QObject::connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept); + if (!dialog.exec()) return; + if (patternInput.text().contains('.')) + { + wcsncpy_s(sp.exportModule, S(patternInput.text()).c_str(), MAX_MODULE_SIZE - 1); + sp.length = 1; + } + else + { + QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8()); + memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), PATTERN_SIZE)); + } + wcsncpy_s(sp.boundaryModule, S(boundInput.text()).c_str(), MAX_MODULE_SIZE - 1); + filter.setPattern(filterInput.text()); + if (!filter.isValid()) filter.setPattern("."); + } + else + { + sp.length = 0; // use default + filter.setPattern(cjkCheckbox.isChecked() ? "[\\x{3000}-\\x{a000}]{4,}" : "[\\x{0020}-\\x{1000}]{4,}"); + } + filter.optimize(); + + auto hooks = std::make_shared(); + try + { + Host::FindHooks(processId, sp, + [hooks, filter](HookParam hp, std::wstring text) { if (filter.match(S(text)).hasMatch()) *hooks << sanitize(S(HookCode::Generate(hp) + L" => " + text)); }); + } + catch (std::out_of_range) { return; } + std::thread([hooks] + { + for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) + lastSize = hooks->size(); + + QString saveFileName; + QMetaObject::invokeMethod(This, [&] + { + auto hookList = new QListView(This); + hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); + hookList->setAttribute(Qt::WA_DeleteOnClose); + hookList->resize({ 750, 300 }); + hookList->setWindowTitle(SEARCH_FOR_HOOKS); + if (hooks->size() > 5'000) + { + hookList->setUniformItemSizes(true); // they aren't actually uniform, but this improves performance + hooks->push_back(QString(2000, '-')); // dumb hack: with uniform item sizes, the last item is assumed to be the largest + } + hookList->setModel(new QStringListModel(*hooks, hookList)); + QObject::connect(hookList, &QListView::clicked, [](QModelIndex i) { AddHook(i.data().toString().split(" => ")[0]); }); + hookList->show(); + + saveFileName = QFileDialog::getSaveFileName(This, SAVE_SEARCH_RESULTS, "./results.txt", TEXT_FILES); + }, Qt::BlockingQueuedConnection); + if (!saveFileName.isEmpty()) + { + QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate); + for (auto hook = hooks->cbegin(); hook != hooks->cend(); ++hook) saveFile.write(hook->toUtf8().append('\n')); // QStringList::begin() makes a copy + } + hooks->clear(); + }).detach(); + } + + void Settings() + { + QDialog dialog(This, Qt::WindowCloseButtonHint); + QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog); + QFormLayout layout(&dialog); + QPushButton saveButton(SAVE_SETTINGS, &dialog); + for (auto [value, label] : Array{ + { TextThread::filterRepetition, FILTER_REPETITION }, + { autoAttach, AUTO_ATTACH }, + { autoAttachSavedOnly, ATTACH_SAVED_ONLY }, + { showSystemProcesses, SHOW_SYSTEM_PROCESSES }, + }) + { + auto checkBox = new QCheckBox(&dialog); + checkBox->setChecked(value); + layout.addRow(label, checkBox); + QObject::connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); }); + } + for (auto [value, label] : Array{ + { TextThread::maxBufferSize, MAX_BUFFER_SIZE }, + { TextThread::flushDelay, FLUSH_DELAY }, + { TextThread::maxHistorySize, MAX_HISTORY_SIZE }, + { Host::defaultCodepage, DEFAULT_CODEPAGE }, + }) + { + auto spinBox = new QSpinBox(&dialog); + spinBox->setMaximum(INT_MAX); + spinBox->setValue(value); + layout.addRow(label, spinBox); + QObject::connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); }); + } + layout.addWidget(&saveButton); + QObject::connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept); + dialog.setWindowTitle(SETTINGS); + dialog.exec(); + } + + void Extensions() + { + extenWindow->activateWindow(); + extenWindow->showNormal(); + } + + void ViewThread(int index) + { + ui.ttCombo->setCurrentIndex(index); + ui.textOutput->setPlainText(sanitize(S((current = &Host::GetThread(ParseTextThreadString(ui.ttCombo->itemText(index))))->storage->c_str()))); + ui.textOutput->moveCursor(QTextCursor::End); + } + + void SetOutputFont(QString fontString) + { + QFont font = ui.textOutput->font(); + font.fromString(fontString); + font.setStyleStrategy(QFont::NoFontMerging); + ui.textOutput->setFont(font); + QSettings(CONFIG_FILE, QSettings::IniFormat).setValue(FONT, font.toString()); + } + + void ProcessConnected(DWORD processId) + { + alreadyAttached.insert(processId); + + QString process = S(GetModuleFilename(processId).value_or(L"???")); + QMetaObject::invokeMethod(This, [process, processId] + { + ui.processCombo->addItem(QString::number(processId, 16).toUpper() + ": " + QFileInfo(process).fileName()); + }); + if (process == "???") return; + + // This does add (potentially tons of) duplicates to the file, but as long as I don't perform Ω(N^2) operations it shouldn't be an issue + QTextFile(GAME_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((process + "\n").toUtf8()); + + QStringList allProcesses = QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); + auto hookList = std::find_if(allProcesses.rbegin(), allProcesses.rend(), [&](QString hookList) { return hookList.contains(process); }); + if (hookList != allProcesses.rend()) + for (auto hookInfo : hookList->split(" , ")) + if (auto hp = HookCode::Parse(S(hookInfo))) Host::InsertHook(processId, hp.value()); + else swscanf_s(S(hookInfo).c_str(), L"|%I64d:%I64d:%[^\n]", &savedThreadCtx, &savedThreadCtx2, savedThreadCode, (unsigned)std::size(savedThreadCode)); + } + + void ProcessDisconnected(DWORD processId) + { + QMetaObject::invokeMethod(This, [processId] + { + ui.processCombo->removeItem(ui.processCombo->findText(QString::number(processId, 16).toUpper() + ":", Qt::MatchStartsWith)); + }, Qt::BlockingQueuedConnection); + } + + void ThreadAdded(TextThread& thread) + { + std::wstring threadCode = HookCode::Generate(thread.hp, thread.tp.processId); + bool savedMatch = savedThreadCtx == thread.tp.ctx && savedThreadCtx2 == thread.tp.ctx2 && savedThreadCode == threadCode; + if (savedMatch) + { + savedThreadCtx = savedThreadCtx2 = savedThreadCode[0] = 0; + current = &thread; + } + QMetaObject::invokeMethod(This, [savedMatch, ttString = TextThreadString(thread) + S(FormatString(L" (%s)", threadCode))] + { + ui.ttCombo->addItem(ttString); + if (savedMatch) ViewThread(ui.ttCombo->count() - 1); + }); + } + + void ThreadRemoved(TextThread& thread) + { + QMetaObject::invokeMethod(This, [ttString = TextThreadString(thread)] + { + int threadIndex = ui.ttCombo->findText(ttString, Qt::MatchStartsWith); + if (threadIndex == ui.ttCombo->currentIndex()) ViewThread(0); + ui.ttCombo->removeItem(threadIndex); + }, Qt::BlockingQueuedConnection); + } + + bool SentenceReceived(TextThread& thread, std::wstring& sentence) + { + if (!DispatchSentenceToExtensions(sentence, GetSentenceInfo(thread).data())) return false; + sentence += L'\n'; + if (&thread == current) QMetaObject::invokeMethod(This, [sentence = S(sentence)]() mutable + { + sanitize(sentence); + auto scrollbar = ui.textOutput->verticalScrollBar(); + bool atBottom = scrollbar->value() + 3 > scrollbar->maximum() || (double)scrollbar->value() / scrollbar->maximum() > 0.975; // arbitrary + QTextCursor cursor(ui.textOutput->document()); + cursor.movePosition(QTextCursor::End); + cursor.insertText(sentence); + if (atBottom) scrollbar->setValue(scrollbar->maximum()); + }); + return true; + } + + void OutputContextMenu(QPoint point) + { + std::unique_ptr menu(ui.textOutput->createStandardContextMenu()); + menu->addAction(FONT, [] { if (QString font = QFontDialog::getFont(&ok, ui.textOutput->font(), This, FONT).toString(); ok) SetOutputFont(font); }); + menu->exec(ui.textOutput->mapToGlobal(point)); + } + + void CopyUnlessMouseDown() + { + if (!(QApplication::mouseButtons() & Qt::LeftButton)) ui.textOutput->copy(); + } +} + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) +{ + This = this; + ui.setupUi(this); + extenWindow = new ExtenWindow(this); + for (auto [text, slot] : Array{ + { ATTACH, AttachProcess }, + { LAUNCH, LaunchProcess }, + { DETACH, DetachProcess }, + { FORGET, ForgetProcess }, + { ADD_HOOK, AddHook }, + { REMOVE_HOOKS, RemoveHooks }, + { SAVE_HOOKS, SaveHooks }, + { SEARCH_FOR_HOOKS, FindHooks }, + { SETTINGS, Settings }, + { EXTENSIONS, Extensions } }) { - auto button = new QPushButton(text, ui->processFrame); - connect(button, &QPushButton::clicked, this, slot); - ui->processLayout->addWidget(button); + auto button = new QPushButton(text, ui.processFrame); + connect(button, &QPushButton::clicked, slot); + ui.processLayout->addWidget(button); } - ui->processLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); + ui.processLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); - connect(ui->ttCombo, qOverload(&QComboBox::activated), this, &MainWindow::ViewThread); - connect(ui->textOutput, &QPlainTextEdit::selectionChanged, [this] { if (!(QApplication::mouseButtons() & Qt::LeftButton)) ui->textOutput->copy(); }); - connect(ui->textOutput, &QPlainTextEdit::customContextMenuRequested, this, &MainWindow::OutputContextMenu); + connect(ui.processCombo, qOverload(&QComboBox::currentIndexChanged), [](QString process) + { + selectedProcessId = ui.processCombo->currentText().split(":")[0].toULong(nullptr, 16); + }); + connect(ui.ttCombo, qOverload(&QComboBox::activated), this, ViewThread); + connect(ui.textOutput, &QPlainTextEdit::selectionChanged, this, CopyUnlessMouseDown); + connect(ui.textOutput, &QPlainTextEdit::customContextMenuRequested, this, OutputContextMenu); QSettings settings(CONFIG_FILE, QSettings::IniFormat); if (settings.contains(WINDOW) && QApplication::screenAt(settings.value(WINDOW).toRect().center())) setGeometry(settings.value(WINDOW).toRect()); - SetOutputFont(settings.value(FONT, ui->textOutput->font().toString()).toString()); + SetOutputFont(settings.value(FONT, ui.textOutput->font().toString()).toString()); TextThread::filterRepetition = settings.value(FILTER_REPETITION, TextThread::filterRepetition).toBool(); autoAttach = settings.value(AUTO_ATTACH, autoAttach).toBool(); autoAttachSavedOnly = settings.value(ATTACH_SAVED_ONLY, autoAttachSavedOnly).toBool(); @@ -103,13 +605,7 @@ MainWindow::MainWindow(QWidget *parent) : TextThread::maxHistorySize = settings.value(MAX_HISTORY_SIZE, TextThread::maxHistorySize).toInt(); Host::defaultCodepage = settings.value(DEFAULT_CODEPAGE, Host::defaultCodepage).toInt(); - Host::Start( - [this](DWORD processId) { ProcessConnected(processId); }, - [this](DWORD processId) { ProcessDisconnected(processId); }, - [this](TextThread& thread) { ThreadAdded(thread); }, - [this](TextThread& thread) { ThreadRemoved(thread); }, - [this](TextThread& thread, std::wstring& output) { return SentenceReceived(thread, output); } - ); + Host::Start(ProcessConnected, ProcessDisconnected, ThreadAdded, ThreadRemoved, SentenceReceived); current = &Host::GetThread(Host::console); Host::AddConsoleOutput(ABOUT); @@ -125,7 +621,7 @@ MainWindow::MainWindow(QWidget *parent) : else for (auto [processId, processName] : processes) if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::wstring::npos) Host::InjectProcess(processId); - std::thread([this] + std::thread([] { for (; ; Sleep(10000)) { @@ -156,479 +652,3 @@ void MainWindow::closeEvent(QCloseEvent*) { QCoreApplication::quit(); // Need to do this to kill any windows that might've been made by extensions } - -void MainWindow::ProcessConnected(DWORD processId) -{ - alreadyAttached.insert(processId); - - QString process = S(GetModuleFilename(processId).value_or(L"???")); - QMetaObject::invokeMethod(this, [this, process, processId] - { - ui->processCombo->addItem(QString::number(processId, 16).toUpper() + ": " + QFileInfo(process).fileName()); - }); - if (process == "???") return; - - // This does add (potentially tons of) duplicates to the file, but as long as I don't perform Ω(N^2) operations it shouldn't be an issue - QTextFile(GAME_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((process + "\n").toUtf8()); - - QStringList allProcesses = QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); - auto hookList = std::find_if(allProcesses.rbegin(), allProcesses.rend(), [&](QString hookList) { return hookList.contains(process); }); - if (hookList != allProcesses.rend()) - for (auto hookInfo : hookList->split(" , ")) - if (auto hp = HookCode::Parse(S(hookInfo))) Host::InsertHook(processId, hp.value()); - else swscanf_s(S(hookInfo).c_str(), L"|%I64d:%I64d:%[^\n]", &savedThreadCtx, &savedThreadCtx2, savedThreadCode, (unsigned)std::size(savedThreadCode)); -} - -void MainWindow::ProcessDisconnected(DWORD processId) -{ - QMetaObject::invokeMethod(this, [this, processId] - { - ui->processCombo->removeItem(ui->processCombo->findText(QString::number(processId, 16).toUpper() + ":", Qt::MatchStartsWith)); - }, Qt::BlockingQueuedConnection); -} - -void MainWindow::ThreadAdded(TextThread& thread) -{ - std::wstring threadCode = HookCode::Generate(thread.hp, thread.tp.processId); - bool savedMatch = savedThreadCtx == thread.tp.ctx && savedThreadCtx2 == thread.tp.ctx2 && savedThreadCode == threadCode; - if (savedMatch) - { - savedThreadCtx = savedThreadCtx2 = savedThreadCode[0] = 0; - current = &thread; - } - QMetaObject::invokeMethod(this, [this, savedMatch, ttString = TextThreadString(thread) + S(FormatString(L" (%s)", threadCode))] - { - ui->ttCombo->addItem(ttString); - if (savedMatch) ViewThread(ui->ttCombo->count() - 1); - }); -} - -void MainWindow::ThreadRemoved(TextThread& thread) -{ - QMetaObject::invokeMethod(this, [this, ttString = TextThreadString(thread)] - { - int threadIndex = ui->ttCombo->findText(ttString, Qt::MatchStartsWith); - if (threadIndex == ui->ttCombo->currentIndex()) ViewThread(0); - ui->ttCombo->removeItem(threadIndex); - }, Qt::BlockingQueuedConnection); -} - -bool MainWindow::SentenceReceived(TextThread& thread, std::wstring& sentence) -{ - if (!DispatchSentenceToExtensions(sentence, GetSentenceInfo(thread).data())) return false; - sentence += L'\n'; - if (&thread == current) QMetaObject::invokeMethod(this, [this, sentence = S(sentence)]() mutable - { - sanitize(sentence); - auto scrollbar = ui->textOutput->verticalScrollBar(); - bool atBottom = scrollbar->value() + 3 > scrollbar->maximum() || (double)scrollbar->value() / scrollbar->maximum() > 0.975; // arbitrary - QTextCursor cursor(ui->textOutput->document()); - cursor.movePosition(QTextCursor::End); - cursor.insertText(sentence); - if (atBottom) scrollbar->setValue(scrollbar->maximum()); - }); - return true; -} - -void MainWindow::OutputContextMenu(QPoint point) -{ - std::unique_ptr menu(ui->textOutput->createStandardContextMenu()); - menu->addAction(FONT, [this] { if (QString font = QFontDialog::getFont(&ok, ui->textOutput->font(), this, FONT).toString(); ok) SetOutputFont(font); }); - menu->exec(ui->textOutput->mapToGlobal(point)); -} - -QString MainWindow::TextThreadString(TextThread& thread) -{ - return QString("%1:%2:%3:%4:%5: %6").arg( - QString::number(thread.handle, 16), - QString::number(thread.tp.processId, 16), - QString::number(thread.tp.addr, 16), - QString::number(thread.tp.ctx, 16), - QString::number(thread.tp.ctx2, 16) - ).toUpper().arg(S(thread.name)); -} - -ThreadParam MainWindow::ParseTextThreadString(QString ttString) -{ - QStringList threadParam = ttString.split(":"); - return { threadParam[1].toUInt(nullptr, 16), threadParam[2].toULongLong(nullptr, 16), threadParam[3].toULongLong(nullptr, 16), threadParam[4].toULongLong(nullptr, 16) }; -} - -DWORD MainWindow::GetSelectedProcessId() -{ - return ui->processCombo->currentText().split(":")[0].toULong(nullptr, 16); -} - -std::array MainWindow::GetSentenceInfo(TextThread& thread) -{ - void(*AddSentence)(MainWindow*, int64_t, const wchar_t*) = [](MainWindow* This, int64_t number, const wchar_t* sentence) - { - // pointer from Host::GetThread may not stay valid unless on main thread - QMetaObject::invokeMethod(This, [number, sentence = std::wstring(sentence)] { if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(sentence); }); - }; - - return - { { - { "current select", &thread == current }, - { "text number", thread.handle }, - { "process id", thread.tp.processId }, - { "hook address", (int64_t)thread.tp.addr }, - { "text handle", thread.handle }, - { "text name", (int64_t)thread.name.c_str() }, - { "this", (int64_t)this }, - { "void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)", (int64_t)AddSentence }, - { nullptr, 0 } // nullptr marks end of info array - } }; -} - -std::optional MainWindow::UserSelectedProcess() -{ - QStringList savedProcesses = QString::fromUtf8(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); - std::reverse(savedProcesses.begin(), savedProcesses.end()); - savedProcesses.removeDuplicates(); - savedProcesses.insert(1, FROM_COMPUTER); - QString process = QInputDialog::getItem(this, SELECT_PROCESS, SELECT_PROCESS_INFO, savedProcesses, 0, true, &ok, Qt::WindowCloseButtonHint); - if (process == FROM_COMPUTER) process = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, SELECT_PROCESS, "C:\\", PROCESSES)); - if (ok && process.contains('\\')) return S(process); - return {}; -} - -void MainWindow::AttachProcess() -{ - QMultiHash allProcesses; - for (auto [processId, processName] : GetAllProcesses()) - if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::wstring::npos)) - allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId); - - QStringList processList(allProcesses.uniqueKeys()); - processList.sort(Qt::CaseInsensitive); - if (QString process = QInputDialog::getItem(this, SELECT_PROCESS, ATTACH_INFO, processList, 0, true, &ok, Qt::WindowCloseButtonHint); ok) - if (process.toInt(nullptr, 0)) Host::InjectProcess(process.toInt(nullptr, 0)); - else for (auto processId : allProcesses.values(process)) Host::InjectProcess(processId); -} - -void MainWindow::LaunchProcess() -{ - std::wstring process; - if (auto selected = UserSelectedProcess()) process = selected.value(); - else return; - std::wstring path = std::wstring(process).erase(process.rfind(L'\\')); - - PROCESS_INFORMATION info = {}; - if (!x64 && QMessageBox::question(this, SELECT_PROCESS, USE_JP_LOCALE) == QMessageBox::Yes) - { - if (HMODULE localeEmulator = LoadLibraryW(L"LoaderDll")) - { - // see https://github.com/xupefei/Locale-Emulator/blob/aa99dec3b25708e676c90acf5fed9beaac319160/LEProc/LoaderWrapper.cs#L252 - struct - { - ULONG AnsiCodePage = SHIFT_JIS; - ULONG OemCodePage = SHIFT_JIS; - ULONG LocaleID = LANG_JAPANESE; - ULONG DefaultCharset = SHIFTJIS_CHARSET; - ULONG HookUiLanguageApi = FALSE; - WCHAR DefaultFaceName[LF_FACESIZE] = {}; - TIME_ZONE_INFORMATION Timezone; - ULONG64 Unused = 0; - } LEB; - GetTimeZoneInformation(&LEB.Timezone); - ((LONG(__stdcall*)(decltype(&LEB), LPCWSTR appName, LPWSTR commandLine, LPCWSTR currentDir, void*, void*, PROCESS_INFORMATION*, void*, void*, void*, void*)) - GetProcAddress(localeEmulator, "LeCreateProcess"))(&LEB, process.c_str(), NULL, path.c_str(), NULL, NULL, &info, NULL, NULL, NULL, NULL); - } - } - if (info.hProcess == NULL) - { - STARTUPINFOW DUMMY = { sizeof(DUMMY) }; - CreateProcessW(process.c_str(), NULL, nullptr, nullptr, FALSE, 0, nullptr, path.c_str(), &DUMMY, &info); - } - if (info.hProcess == NULL) return Host::AddConsoleOutput(LAUNCH_FAILED); - Host::InjectProcess(info.dwProcessId); - CloseHandle(info.hProcess); - CloseHandle(info.hThread); -} - -void MainWindow::DetachProcess() -{ - try { Host::DetachProcess(GetSelectedProcessId()); } catch (std::out_of_range) {} -} - -void MainWindow::ForgetProcess() -{ - std::optional processName = GetModuleFilename(GetSelectedProcessId()); - if (!processName) processName = UserSelectedProcess(); - DetachProcess(); - if (!processName) return; - for (auto file : { GAME_SAVE_FILE, HOOK_SAVE_FILE }) - { - QStringList lines = QString::fromUtf8(QTextFile(file, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts); - lines.erase(std::remove_if(lines.begin(), lines.end(), [&](const QString& line) { return line.contains(S(processName.value())); }), lines.end()); - QTextFile(file, QIODevice::WriteOnly | QIODevice::Truncate).write(lines.join("\n").append("\n").toUtf8()); - } -} - -void MainWindow::AddHook() -{ - AddHook(""); -} - -void MainWindow::AddHook(QString hook) -{ - if (QString hookCode = QInputDialog::getText(this, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, hook, &ok, Qt::WindowCloseButtonHint); ok) - if (hookCode.startsWith("S") || hookCode.startsWith("/S")) FindHooks(); - else if (auto hp = HookCode::Parse(S(hookCode))) try { Host::InsertHook(GetSelectedProcessId(), hp.value()); } catch (std::out_of_range) {} - else Host::AddConsoleOutput(INVALID_CODE); -} - -void MainWindow::RemoveHooks() -{ - DWORD processId = GetSelectedProcessId(); - std::unordered_map hooks; - for (int i = 0; i < ui->ttCombo->count(); ++i) - { - ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); - if (tp.processId == GetSelectedProcessId()) hooks[tp.addr] = Host::GetThread(tp).hp; - } - auto hookList = new QListWidget(this); - hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); - hookList->setAttribute(Qt::WA_DeleteOnClose); - hookList->setMinimumSize({ 300, 50 }); - hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK); - for (auto [address, hp] : hooks) - new QListWidgetItem(QString(hp.name) + "@" + QString::number(address, 16), hookList); - connect(hookList, &QListWidget::itemDoubleClicked, [processId, hookList](QListWidgetItem* item) - { - try - { - Host::RemoveHook(processId, item->text().split("@")[1].toULongLong(nullptr, 16)); - delete item; - } - catch (std::out_of_range) { hookList->close(); } - }); - hookList->show(); -} - -void MainWindow::SaveHooks() -{ - auto processName = GetModuleFilename(GetSelectedProcessId()); - if (!processName) return; - QHash hookCodes; - for (int i = 0; i < ui->ttCombo->count(); ++i) - { - ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); - if (tp.processId == GetSelectedProcessId()) - { - HookParam hp = Host::GetThread(tp).hp; - if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(HookCode::Generate(hp, tp.processId)); - } - } - auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); - ThreadParam tp = current->tp; - if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(HookCode::Generate(current->hp, tp.processId))); - QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); -} - -void MainWindow::FindHooks() -{ - QMessageBox::information(this, SEARCH_FOR_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING); - - DWORD processId = GetSelectedProcessId(); - SearchParam sp = {}; - sp.codepage = Host::defaultCodepage; - bool searchForText = false, customSettings = false; - QRegularExpression filter(".", QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::DotMatchesEverythingOption); - - QDialog dialog(this, Qt::WindowCloseButtonHint); - QFormLayout layout(&dialog); - QCheckBox cjkCheckbox(&dialog); - layout.addRow(SEARCH_CJK, &cjkCheckbox); - QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help | QDialogButtonBox::Retry, &dialog); - layout.addRow(&confirm); - confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH); - confirm.button(QDialogButtonBox::Retry)->setText(SEARCH_FOR_TEXT); - confirm.button(QDialogButtonBox::Help)->setText(SETTINGS); - connect(&confirm, &QDialogButtonBox::clicked, [&](QAbstractButton* button) - { - if (button == confirm.button(QDialogButtonBox::Retry)) searchForText = true; - if (button == confirm.button(QDialogButtonBox::Help)) customSettings = true; - dialog.accept(); - }); - dialog.setWindowTitle(SEARCH_FOR_HOOKS); - if (!dialog.exec()) return; - - if (searchForText) - { - QDialog dialog(this, Qt::WindowCloseButtonHint); - QFormLayout layout(&dialog); - QLineEdit textInput(&dialog); - layout.addRow(TEXT, &textInput); - QSpinBox codepageInput(&dialog); - codepageInput.setMaximum(INT_MAX); - codepageInput.setValue(sp.codepage); - layout.addRow(CODEPAGE, &codepageInput); - QDialogButtonBox confirm(QDialogButtonBox::Ok); - connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - layout.addRow(&confirm); - if (!dialog.exec()) return; - wcsncpy_s(sp.text, S(textInput.text()).c_str(), PATTERN_SIZE - 1); - try { Host::FindHooks(GetSelectedProcessId(), sp); } - catch (std::out_of_range) {} - return; - } - - if (customSettings) - { - QDialog dialog(this, Qt::WindowCloseButtonHint); - QFormLayout layout(&dialog); - QLineEdit patternInput(x64 ? "CC CC 48 89" : "55 8B EC", &dialog); - assert(QByteArray::fromHex(patternInput.text().toUtf8()) == QByteArray((const char*)sp.pattern, sp.length)); - layout.addRow(SEARCH_PATTERN, &patternInput); - for (auto [value, label] : Array{ - { sp.searchTime, SEARCH_DURATION }, - { sp.offset, PATTERN_OFFSET }, - { sp.maxRecords, MAX_HOOK_SEARCH_RECORDS }, - { sp.codepage, CODEPAGE }, - }) - { - auto spinBox = new QSpinBox(&dialog); - spinBox->setMaximum(INT_MAX); - spinBox->setValue(value); - layout.addRow(label, spinBox); - connect(spinBox, qOverload(&QSpinBox::valueChanged), [&value](int newValue) { value = newValue; }); - } - QLineEdit boundInput(QFileInfo(S(GetModuleFilename(GetSelectedProcessId()).value_or(L""))).fileName(), &dialog); - layout.addRow(SEARCH_MODULE, &boundInput); - for (auto [value, label] : Array{ - { sp.minAddress, MIN_ADDRESS }, - { sp.maxAddress, MAX_ADDRESS }, - { sp.padding, STRING_OFFSET }, - }) - { - auto input = new QLineEdit(QString::number(value, 16), &dialog); - layout.addRow(label, input); - connect(input, &QLineEdit::textEdited, [&value](QString text) { if (uintptr_t newValue = text.toULongLong(&ok, 16); ok) value = newValue; }); - } - QLineEdit filterInput(filter.pattern(), &dialog); - layout.addRow(HOOK_SEARCH_FILTER, &filterInput); - QPushButton startButton(START_HOOK_SEARCH, &dialog); - layout.addWidget(&startButton); - connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept); - if (!dialog.exec()) return; - if (patternInput.text().contains('.')) - { - wcsncpy_s(sp.exportModule, S(patternInput.text()).c_str(), MAX_MODULE_SIZE - 1); - sp.length = 1; - } - else - { - QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8()); - memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), PATTERN_SIZE)); - } - wcsncpy_s(sp.boundaryModule, S(boundInput.text()).c_str(), MAX_MODULE_SIZE - 1); - filter.setPattern(filterInput.text()); - if (!filter.isValid()) filter.setPattern("."); - } - else - { - sp.length = 0; // use default - filter.setPattern(cjkCheckbox.isChecked() ? "[\\x{3000}-\\x{a000}]{4,}" : "[\\x{0020}-\\x{1000}]{4,}"); - } - filter.optimize(); - - auto hooks = std::make_shared(); - try - { - Host::FindHooks(processId, sp, - [hooks, filter](HookParam hp, std::wstring text) { if (filter.match(S(text)).hasMatch()) *hooks << sanitize(S(HookCode::Generate(hp) + L" => " + text)); }); - } - catch (std::out_of_range) { return; } - std::thread([this, hooks] - { - for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) - lastSize = hooks->size(); - - QString saveFileName; - QMetaObject::invokeMethod(this, [&] - { - auto hookList = new QListView(this); - hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); - hookList->setAttribute(Qt::WA_DeleteOnClose); - hookList->resize({ 750, 300 }); - hookList->setWindowTitle(SEARCH_FOR_HOOKS); - if (hooks->size() > 5'000) - { - hookList->setUniformItemSizes(true); // they aren't actually uniform, but this improves performance - hooks->push_back(QString(2000, '-')); // dumb hack: with uniform item sizes, the last item is assumed to be the largest - } - hookList->setModel(new QStringListModel(*hooks, hookList)); - connect(hookList, &QListView::clicked, [this](QModelIndex i) { AddHook(i.data().toString().split(" => ")[0]); }); - hookList->show(); - - saveFileName = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./results.txt", TEXT_FILES); - }, Qt::BlockingQueuedConnection); - if (!saveFileName.isEmpty()) - { - QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate); - for (auto hook = hooks->cbegin(); hook != hooks->cend(); ++hook) saveFile.write(hook->toUtf8().append('\n')); // QStringList::begin() makes a copy - } - hooks->clear(); - }).detach(); -} - -void MainWindow::Settings() -{ - QDialog dialog(this, Qt::WindowCloseButtonHint); - QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog); - QFormLayout layout(&dialog); - QPushButton saveButton(SAVE_SETTINGS, &dialog); - for (auto [value, label] : Array{ - { TextThread::filterRepetition, FILTER_REPETITION }, - { autoAttach, AUTO_ATTACH }, - { autoAttachSavedOnly, ATTACH_SAVED_ONLY }, - { showSystemProcesses, SHOW_SYSTEM_PROCESSES }, - }) - { - auto checkBox = new QCheckBox(&dialog); - checkBox->setChecked(value); - layout.addRow(label, checkBox); - connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); }); - } - for (auto [value, label] : Array{ - { TextThread::maxBufferSize, MAX_BUFFER_SIZE }, - { TextThread::flushDelay, FLUSH_DELAY }, - { TextThread::maxHistorySize, MAX_HISTORY_SIZE }, - { Host::defaultCodepage, DEFAULT_CODEPAGE }, - }) - { - auto spinBox = new QSpinBox(&dialog); - spinBox->setMaximum(INT_MAX); - spinBox->setValue(value); - layout.addRow(label, spinBox); - connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); }); - } - layout.addWidget(&saveButton); - connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept); - dialog.setWindowTitle(SETTINGS); - dialog.exec(); -} - -void MainWindow::Extensions() -{ - extenWindow->activateWindow(); - extenWindow->showNormal(); -} - -void MainWindow::ViewThread(int index) -{ - ui->ttCombo->setCurrentIndex(index); - ui->textOutput->setPlainText(sanitize(S((current = &Host::GetThread(ParseTextThreadString(ui->ttCombo->itemText(index))))->storage->c_str()))); - ui->textOutput->moveCursor(QTextCursor::End); -} - -void MainWindow::SetOutputFont(QString fontString) -{ - QFont font = ui->textOutput->font(); - font.fromString(fontString); - font.setStyleStrategy(QFont::NoFontMerging); - ui->textOutput->setFont(font); - QSettings(CONFIG_FILE, QSettings::IniFormat).setValue(FONT, font.toString()); -} diff --git a/GUI/mainwindow.h b/GUI/mainwindow.h index 198b93b..13822aa 100644 --- a/GUI/mainwindow.h +++ b/GUI/mainwindow.h @@ -1,56 +1,12 @@ #pragma once #include "qtcommon.h" -#include "extenwindow.h" -#include "host/host.h" - -namespace Ui -{ - class MainWindow; -} class MainWindow : public QMainWindow { public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); - private: - inline static constexpr auto HOOK_SAVE_FILE = u8"SavedHooks.txt"; - inline static constexpr auto GAME_SAVE_FILE = u8"SavedGames.txt"; - - void closeEvent(QCloseEvent*) override; - void ProcessConnected(DWORD processId); - void ProcessDisconnected(DWORD processId); - void ThreadAdded(TextThread& thread); - void ThreadRemoved(TextThread& thread); - bool SentenceReceived(TextThread& thread, std::wstring& sentence); - void OutputContextMenu(QPoint point); - QString TextThreadString(TextThread& thread); - ThreadParam ParseTextThreadString(QString ttString); - DWORD GetSelectedProcessId(); - std::array GetSentenceInfo(TextThread& thread); - std::optional UserSelectedProcess(); - void AttachProcess(); - void LaunchProcess(); - void DetachProcess(); - void ForgetProcess(); - void AddHook(); - void AddHook(QString hook); - void RemoveHooks(); - void SaveHooks(); - void FindHooks(); - void Settings(); - void Extensions(); - void ViewThread(int index); - void SetOutputFont(QString font); - - Ui::MainWindow* ui; - ExtenWindow* extenWindow; - std::unordered_set alreadyAttached; - bool autoAttach = false, autoAttachSavedOnly = true; - bool showSystemProcesses = false; - uint64_t savedThreadCtx = 0, savedThreadCtx2 = 0; - wchar_t savedThreadCode[1000] = {}; - TextThread* current = nullptr; + void closeEvent(QCloseEvent*); };