Merge pull request #416 from zeheyler/devtools_deepl_extension

add devtools api and new deepl extension
This commit is contained in:
Akash Mozumdar 2021-01-15 09:35:03 -07:00 committed by GitHub
commit dcff7e550c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 378 additions and 111 deletions

View File

@ -14,10 +14,13 @@ add_executable(Textractor WIN32
Textractor.ico
)
target_precompile_headers(Textractor REUSE_FROM pch)
target_link_libraries(${PROJECT_NAME} Qt5::Widgets shell32 winhttp)
target_link_libraries(Textractor Qt5::Widgets shell32 winhttp)
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5Core.dll)
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5Cored.dll)
install_qt5_libs(${PROJECT_NAME})
endif()
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5Core.dll AND NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5Cored.dll)
add_custom_command(TARGET Textractor
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/windeployqt"
COMMAND set PATH=%PATH%$<SEMICOLON>${qt5_install_prefix}/bin
COMMAND Qt5::windeployqt --dir ${CMAKE_FINAL_OUTPUT_DIRECTORY} "${CMAKE_FINAL_OUTPUT_DIRECTORY}/Textractor.exe"
)
endif()

View File

@ -138,8 +138,6 @@ ExtenWindow::ExtenWindow(QWidget* parent) :
Sync();
}
ExtenWindow::~ExtenWindow() = default;
bool ExtenWindow::eventFilter(QObject* target, QEvent* event)
{
// See https://stackoverflow.com/questions/1224432/how-do-i-respond-to-an-internal-drag-and-drop-operation-using-a-qlistwidget/1528215

View File

@ -20,7 +20,6 @@ class ExtenWindow : public QMainWindow
{
public:
explicit ExtenWindow(QWidget* parent = nullptr);
~ExtenWindow();
private:
bool eventFilter(QObject* target, QEvent* event) override;

View File

@ -129,8 +129,7 @@ namespace
{
size_t size = 0;
int value = 0;
try { value = std::stoi(HCode, &size, 16); }
catch (std::invalid_argument) {}
try { value = std::stoi(HCode, &size, 16); } catch (std::invalid_argument) {}
HCode.erase(0, size);
return value;
};

View File

@ -32,8 +32,7 @@ namespace
{
if (!view) return {};
std::scoped_lock lock(viewMutex);
for (auto hook : view)
if (hook.address == addr) return hook;
for (auto hook : view) if (hook.address == addr) return hook;
return {};
}
@ -69,8 +68,7 @@ namespace
void RemoveThreads(std::function<bool(ThreadParam)> removeIf)
{
std::vector<TextThread*> threadsToRemove;
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents)
if (removeIf(tp)) threadsToRemove.push_back(&thread);
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (removeIf(tp)) threadsToRemove.push_back(&thread);
for (auto thread : threadsToRemove)
{
OnDestroy(*thread);
@ -250,8 +248,7 @@ namespace Host
TextThread* GetThread(int64_t handle)
{
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents)
if (thread.handle == handle) return &thread;
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (thread.handle == handle) return &thread;
return nullptr;
}

View File

@ -22,7 +22,7 @@ TextThread::TextThread(ThreadParam tp, HookParam hp, std::optional<std::wstring>
void TextThread::Start()
{
CreateTimerQueueTimer(&timer, NULL, [](void* This, BOOLEAN) { ((TextThread*)This)->Flush(); }, this, 10, 10, WT_EXECUTELONGFUNCTION);
CreateTimerQueueTimer(&timer, NULL, [](void* This, auto) { ((TextThread*)This)->Flush(); }, this, 10, 10, WT_EXECUTELONGFUNCTION);
}
void TextThread::Stop()
@ -42,8 +42,21 @@ void TextThread::Push(BYTE* data, int length)
BYTE doubleByteChar[2];
if (length == 1) // doublebyte characters must be processed as pairs
if (leadByte) std::tie(doubleByteChar[0], doubleByteChar[1], data, length, leadByte) = std::tuple(leadByte, data[0], doubleByteChar, 2, 0);
else if (IsDBCSLeadByteEx(hp.codepage ? hp.codepage : Host::defaultCodepage, data[0])) std::tie(leadByte, length) = std::tuple(data[0], 0);
{
if (leadByte)
{
doubleByteChar[0] = leadByte;
doubleByteChar[1] = data[0];
data = doubleByteChar;
length = 2;
leadByte = 0;
}
else if (IsDBCSLeadByteEx(hp.codepage ? hp.codepage : Host::defaultCodepage, data[0]))
{
leadByte = data[0];
length = 0;
}
}
if (hp.type & HEX_DUMP) for (int i = 0; i < length; i += sizeof(short)) buffer.append(FormatString(L"%04hX ", *(short*)(data + i)));
else if (hp.type & USING_UNICODE) buffer.append((wchar_t*)data, length / sizeof(wchar_t));

View File

@ -17,7 +17,7 @@
extern const char* ATTACH;
extern const char* LAUNCH;
extern const char* GAME_CONFIG;
extern const char* CONFIG;
extern const char* DETACH;
extern const char* FORGET;
extern const char* ADD_HOOK;
@ -198,14 +198,14 @@ namespace
CloseHandle(info.hThread);
}
void OpenProcessConfig()
void ConfigureProcess()
{
if (auto processName = GetModuleFilename(selectedProcessId)) if (int last = processName->rfind(L'\\') + 1)
{
std::wstring configFile = std::wstring(processName.value()).replace(last, std::string::npos, GAME_CONFIG_FILE);
if (!std::filesystem::exists(configFile)) QTextFile(S(configFile), QFile::WriteOnly).write("see https://github.com/Artikash/Textractor/wiki/Game-configuration-file");
if (std::filesystem::exists(configFile)) _wspawnlp(_P_DETACH, L"notepad", L"notepad", configFile.c_str(), NULL);
else QMessageBox::critical(This, GAME_CONFIG, QString(FAILED_TO_CREATE_CONFIG_FILE).arg(S(configFile)));
else QMessageBox::critical(This, CONFIG, QString(FAILED_TO_CREATE_CONFIG_FILE).arg(S(configFile)));
}
}
@ -258,8 +258,7 @@ namespace
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);
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
@ -335,8 +334,7 @@ namespace
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) {}
try { Host::FindHooks(selectedProcessId, sp); } catch (std::out_of_range) {}
return;
}
@ -408,8 +406,7 @@ namespace
catch (std::out_of_range) { return; }
std::thread([hooks]
{
for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000))
lastSize = hooks->size();
for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size();
QString saveFileName;
QMetaObject::invokeMethod(This, [&]
@ -562,6 +559,7 @@ namespace
bool SentenceReceived(TextThread& thread, std::wstring& sentence)
{
for (int i = 0; i < sentence.size(); ++i) if (sentence[i] == '\r' && sentence[i + 1] == '\n') sentence[i] = 0x200b; // for some reason \r appears as newline - no need to double
if (!DispatchSentenceToExtensions(sentence, GetSentenceInfo(thread).data())) return false;
sentence += L'\n';
if (&thread == current) QMetaObject::invokeMethod(This, [sentence = S(sentence)]() mutable
@ -598,7 +596,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
for (auto [text, slot] : Array<const char*, void(&)()>{
{ ATTACH, AttachProcess },
{ LAUNCH, LaunchProcess },
{ GAME_CONFIG, OpenProcessConfig },
{ CONFIG, ConfigureProcess },
{ DETACH, DetachProcess },
{ FORGET, ForgetProcess },
{ ADD_HOOK, AddHook },
@ -647,7 +645,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
for (int i = 0; i < argc; ++i)
if (std::wstring arg = argv[i]; arg[0] == L'/' || arg[0] == L'-')
if (arg[1] == L'p' || arg[1] == L'P')
if (DWORD processId = _wtoi(arg.substr(2).c_str())) Host::InjectProcess(processId);
if (DWORD processId = wcstoul(arg.substr(2).c_str(), nullptr, 0)) Host::InjectProcess(processId);
else for (auto [processId, processName] : processes)
if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::string::npos) Host::InjectProcess(processId);

View File

@ -87,23 +87,3 @@ macro(find_qt5)
message(FATAL_ERROR "Cannot find QT5!")
endif()
endmacro(find_qt5)
# Copies required DLLs to directory with target
# Optionally can provide QML directory as second argument
function(install_qt5_libs target)
if(TARGET Qt5::windeployqt)
set(EXTRA "")
if(EXISTS ${ARGV1})
message("QML directory to be scanned=${ARGV1}")
list(APPEND EXTRA --qmldir ${ARGV1})
endif()
# execute windeployqt in a tmp directory after build
add_custom_command(TARGET ${target}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/windeployqt"
COMMAND set PATH=%PATH%$<SEMICOLON>${qt5_install_prefix}/bin
COMMAND Qt5::windeployqt --dir $<TARGET_FILE_DIR:${target}> "$<TARGET_FILE_DIR:${target}>/$<TARGET_FILE_NAME:${target}>" ${EXTRA}
)
endif()
endfunction(install_qt5_libs)

View File

@ -1,12 +1,12 @@
include(QtUtils)
msvc_registry_search()
find_qt5(Core Widgets)
find_qt5(Core Widgets WebSockets)
cmake_policy(SET CMP0037 OLD)
add_library(Bing\ Translate MODULE bingtranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
add_library(Copy\ to\ Clipboard MODULE copyclipboard.cpp extensionimpl.cpp)
add_library(DeepL\ Translate MODULE deepltranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
add_library(DevTools\ DeepL\ Translate MODULE devtoolsdeepltranslate.cpp devtools.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
add_library(Extra\ Newlines MODULE extranewlines.cpp extensionimpl.cpp)
add_library(Extra\ Window MODULE extrawindow.cpp extensionimpl.cpp)
add_library(Google\ Translate MODULE googletranslate.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
@ -22,6 +22,7 @@ add_library(Thread\ Linker MODULE threadlinker.cpp extensionimpl.cpp)
target_precompile_headers(Bing\ Translate REUSE_FROM pch)
target_precompile_headers(Copy\ to\ Clipboard REUSE_FROM pch)
target_precompile_headers(DeepL\ Translate REUSE_FROM pch)
target_precompile_headers(DevTools\ DeepL\ Translate REUSE_FROM pch)
target_precompile_headers(Extra\ Newlines REUSE_FROM pch)
target_precompile_headers(Extra\ Window REUSE_FROM pch)
target_precompile_headers(Google\ Translate REUSE_FROM pch)
@ -36,8 +37,18 @@ target_precompile_headers(Thread\ Linker REUSE_FROM pch)
target_link_libraries(Bing\ Translate winhttp Qt5::Widgets)
target_link_libraries(DeepL\ Translate winhttp Qt5::Widgets)
target_link_libraries(DevTools\ DeepL\ Translate shell32 winhttp Qt5::Widgets Qt5::WebSockets)
target_link_libraries(Extra\ Window Qt5::Widgets)
target_link_libraries(Google\ Translate winhttp Qt5::Widgets)
target_link_libraries(Lua lua53 Qt5::Widgets)
target_link_libraries(Regex\ Filter Qt5::Widgets)
target_link_libraries(Thread\ Linker Qt5::Widgets)
if (NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5WebSockets.dll AND NOT EXISTS ${CMAKE_FINAL_OUTPUT_DIRECTORY}/Qt5WebSocketsd.dll)
add_custom_command(TARGET DevTools\ DeepL\ Translate
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/windeployqt"
COMMAND set PATH=%PATH%$<SEMICOLON>${qt5_install_prefix}/bin
COMMAND Qt5::windeployqt --dir ${CMAKE_FINAL_OUTPUT_DIRECTORY} "${CMAKE_FINAL_OUTPUT_DIRECTORY}/DevTools\ DeepL\ Translate.dll"
)
endif()

View File

@ -1,5 +1,4 @@
#include "extension.h"
#include "network.h"
#include "network.h"
#include <QStringList>
extern const wchar_t* TRANSLATION_ERROR;
@ -81,7 +80,7 @@ QStringList languages
};
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 500;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text)
{
@ -94,10 +93,8 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text)
FormatString(R"([{"text":"%s"}])", JSON::Escape(WideStringToString(text))),
FormatString(L"Content-Type: application/json; charset=UTF-8\r\nOcp-Apim-Subscription-Key:%s", apiKey.Copy()).c_str()
})
{
if (auto translation = Copy(JSON::Parse(httpRequest.response)[0][L"translations"][0][L"text"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
}
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
if (HttpRequest httpRequest{

View File

@ -1,5 +1,4 @@
#include "qtcommon.h"
#include "extension.h"
#include "network.h"
#include <random>
@ -26,7 +25,7 @@ QStringList languages
};
bool translateSelectedOnly = true, rateLimitAll = true, rateLimitSelected = true, useCache = true;
int tokenCount = 10, tokenRestoreDelay = 60000, maxSentenceSize = 500;
int tokenCount = 10, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
enum KeyType { CAT, REST };
int keyType = CAT;
@ -90,6 +89,7 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text)
L"/jsonrpc",
body,
L"Host: www2.deepl.com\r\nAccept-Language: en-US,en;q=0.5\r\nContent-type: application/json; charset=utf-8\r\nOrigin: https://www.deepl.com\r\nTE: Trailers",
INTERNET_DEFAULT_PORT,
L"https://www.deepl.com/translator",
WINHTTP_FLAG_SECURE
})

116
extensions/devtools.cpp Normal file
View File

@ -0,0 +1,116 @@
#include "devtools.h"
#include <QWebSocket>
#include <QMetaEnum>
#include <ppltasks.h>
namespace
{
std::function<void(QString)> OnStatusChanged = Swallow;
PROCESS_INFORMATION processInfo = {};
std::atomic<int> idCounter = 0;
std::mutex devToolsMutex;
QWebSocket webSocket;
std::unordered_map<int, concurrency::task_completion_event<JSON::Value<wchar_t>>> mapQueue;
auto _ = ([]
{
QObject::connect(&webSocket, &QWebSocket::stateChanged,
[](QAbstractSocket::SocketState state) { OnStatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); }
);
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
{
auto result = JSON::Parse(S(message));
std::scoped_lock lock(devToolsMutex);
if (auto id = result[L"id"].Number()) if (auto request = mapQueue.find((int)*id); request != mapQueue.end())
{
request->second.set(result);
mapQueue.erase(request);
}
});
}(), 0);
}
namespace DevTools
{
void Start(const std::wstring& path, std::function<void(QString)> statusChanged, bool headless)
{
OnStatusChanged = statusChanged;
DWORD exitCode = 0;
auto args = FormatString(
L"%s --proxy-server=direct:// --disable-extensions --disable-gpu --user-data-dir=%s\\devtoolscache --remote-debugging-port=9222",
path,
std::filesystem::current_path().wstring()
);
if (headless) args += L" --headless";
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
if ((GetExitCodeProcess(processInfo.hProcess, &exitCode) && exitCode == STILL_ACTIVE) ||
CreateProcessW(NULL, args.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &DUMMY, &processInfo)
)
{
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"127.0.0.1",
L"POST",
L"/json/list",
"",
NULL,
9222,
NULL,
WINHTTP_FLAG_ESCAPE_DISABLE
})
{
if (auto list = Copy(JSON::Parse(httpRequest.response).Array())) if (auto it = std::find_if(
list->begin(),
list->end(),
[](const JSON::Value<wchar_t>& object) { return object[L"type"].String() && *object[L"type"].String() == L"page" && object[L"webSocketDebuggerUrl"].String(); }
); it != list->end())
{
std::scoped_lock lock(devToolsMutex);
webSocket.open(S(*(*it)[L"webSocketDebuggerUrl"].String()));
return;
}
}
OnStatusChanged("Failed Connection");
}
else OnStatusChanged("Failed Startup");
}
void Close()
{
std::scoped_lock lock(devToolsMutex);
for (const auto& [_, task] : mapQueue) task.set_exception(std::runtime_error("closed"));
webSocket.close();
mapQueue.clear();
DWORD exitCode = 0;
if (GetExitCodeProcess(processInfo.hProcess, &exitCode) && exitCode == STILL_ACTIVE)
{
TerminateProcess(processInfo.hProcess, 0);
WaitForSingleObject(processInfo.hProcess, 100);
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
try { std::filesystem::remove_all(L"devtoolscache"); } catch (std::filesystem::filesystem_error) {}
OnStatusChanged("Stopped");
}
bool Connected()
{
std::scoped_lock lock(devToolsMutex);
return webSocket.state() == QAbstractSocket::ConnectedState;
}
JSON::Value<wchar_t> SendRequest(const std::wstring& method, const std::wstring& params)
{
concurrency::task_completion_event<JSON::Value<wchar_t>> response;
int id = idCounter += 1;
auto message = FormatString(LR"({"id":%d,"method":"%s","params":%s})", id, method, params);
{
std::scoped_lock lock(devToolsMutex);
if (webSocket.state() != QAbstractSocket::ConnectedState) return {};
mapQueue.try_emplace(id, response);
webSocket.sendTextMessage(S(message));
webSocket.flush();
}
try { if (auto result = create_task(response).get()[L"result"]) return result; } catch (...) {}
return {};
}
}

10
extensions/devtools.h Normal file
View File

@ -0,0 +1,10 @@
#include "qtcommon.h"
#include "network.h"
namespace DevTools
{
void Start(const std::wstring& path, std::function<void(QString)> statusChanged, bool headless);
void Close();
bool Connected();
JSON::Value<wchar_t> SendRequest(const std::wstring& method, const std::wstring& params = L"{}");
}

View File

@ -0,0 +1,124 @@
#include "qtcommon.h"
#include "devtools.h"
#include <ShlObj.h>
extern const wchar_t* TRANSLATION_ERROR;
extern Synchronized<std::wstring> translateTo;
extern QFormLayout* display;
extern Settings settings;
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
const char* GET_API_KEY_FROM = nullptr;
bool translateSelectedOnly = true, rateLimitAll = false, rateLimitSelected = false, useCache = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 2500;
extern const char* CHROME_LOCATION;
extern const char* START_DEVTOOLS;
extern const char* STOP_DEVTOOLS;
extern const char* HEADLESS_MODE;
extern const char* DEVTOOLS_STATUS;
extern const char* AUTO_START;
extern const wchar_t* ERROR_START_CHROME;
QStringList languages
{
"Chinese (simplified): zh",
"Dutch: nl",
"English: en",
"French: fr",
"German: de",
"Italian: it",
"Japanese: ja",
"Polish: pl",
"Portuguese: pt",
"Russian: ru",
"Spanish: es",
};
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
QString chromePath = settings.value(CHROME_LOCATION).toString();
wchar_t programFiles[MAX_PATH + 100] = {};
if (chromePath.isEmpty()) for (auto folder : { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES, CSIDL_LOCAL_APPDATA })
{
SHGetFolderPathW(NULL, folder, NULL, SHGFP_TYPE_CURRENT, programFiles);
wcscat_s(programFiles, L"/Google/Chrome/Application/chrome.exe");
if (std::filesystem::exists(programFiles)) chromePath = S(programFiles);
}
auto chromePathEdit = new QLineEdit(chromePath);
QObject::connect(chromePathEdit, &QLineEdit::textChanged, [chromePathEdit](QString path) { settings.setValue(CHROME_LOCATION, path); });
display->addRow(CHROME_LOCATION, chromePathEdit);
auto statusLabel = new QLabel("Stopped");
auto startButton = new QPushButton(START_DEVTOOLS), stopButton = new QPushButton(STOP_DEVTOOLS);
auto headlessCheckBox = new QCheckBox();
headlessCheckBox->setChecked(settings.value(HEADLESS_MODE, true).toBool());
QObject::connect(headlessCheckBox, &QCheckBox::clicked, [](bool headless) { settings.setValue(HEADLESS_MODE, headless); });
QObject::connect(startButton, &QPushButton::clicked, [statusLabel, chromePathEdit, headlessCheckBox] {
DevTools::Start(
S(chromePathEdit->text()),
[statusLabel](QString status)
{
QMetaObject::invokeMethod(statusLabel, std::bind(&QLabel::setText, statusLabel, status));
if (status == "ConnectedState") std::thread([]
{
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"127.0.0.1",
L"POST",
L"/json/version",
"",
NULL,
9222,
NULL,
WINHTTP_FLAG_ESCAPE_DISABLE
})
if (auto userAgent = Copy(JSON::Parse(httpRequest.response)[L"User-Agent"].String()))
if (userAgent->find(L"Headless") != std::string::npos)
DevTools::SendRequest(L"Network.setUserAgentOverride",
FormatString(LR"({"userAgent":"%s"})", userAgent->replace(userAgent->find(L"Headless"), 8, L"")));
}).detach();
},
headlessCheckBox->isChecked()
);
});
QObject::connect(stopButton, &QPushButton::clicked, &DevTools::Close);
auto buttons = new QHBoxLayout();
buttons->addWidget(startButton);
buttons->addWidget(stopButton);
display->addRow(HEADLESS_MODE, headlessCheckBox);
auto autoStartButton = new QCheckBox();
autoStartButton->setChecked(settings.value(AUTO_START, false).toBool());
QObject::connect(autoStartButton, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
display->addRow(AUTO_START, autoStartButton);
display->addRow(buttons);
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
display->addRow(DEVTOOLS_STATUS, statusLabel);
if (autoStartButton->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
}
break;
case DLL_PROCESS_DETACH:
{
DevTools::Close();
}
break;
}
return TRUE;
}
std::pair<bool, std::wstring> Translate(const std::wstring& text)
{
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
// DevTools can't handle concurrent translations yet
static std::mutex translationMutex;
std::scoped_lock lock(translationMutex);
DevTools::SendRequest(L"Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/translator#any/%s/%s"})", translateTo.Copy(), Escape(text)));
for (int retry = 0; ++retry < 100; Sleep(100))
if (auto translation = Copy(DevTools::SendRequest(
L"Runtime.evaluate", LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML","returnByValue":true})")[L"result"][L"value"].String())
) if (!translation->empty()) return { true, translation.value() };
return { false, TRANSLATION_ERROR };
}

View File

@ -46,7 +46,7 @@ struct PrettyWindow : QDialog
{
PrettyWindow(const char* name)
{
localize();
Localize();
ui.setupUi(this);
ui.display->setGraphicsEffect(&outliner);
setWindowFlags(Qt::FramelessWindowHint);
@ -439,8 +439,7 @@ private:
{
std::vector<LookupResult> results;
for (auto [it, end] = std::equal_range(dictionary.begin(), dictionary.end(), DictionaryEntry{ term.toUtf8() }); it != end; ++it)
if (foundDefinitions.emplace(it->definition).second)
results.push_back({ term, it->definition, inflectionsUsed });
if (foundDefinitions.emplace(it->definition).second) results.push_back({ term, it->definition, inflectionsUsed });
for (const auto& inflection : inflections) if (auto match = inflection.inflectsTo.match(term); match.hasMatch())
{
QStringList currentInflectionsUsed = inflectionsUsed;

View File

@ -1,5 +1,4 @@
#include "qtcommon.h"
#include "extension.h"
#include "network.h"
#include <ctime>
@ -123,7 +122,7 @@ QStringList languages
};
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 500;
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text)
{
@ -153,9 +152,11 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text)
if (auto blob = Copy(JSON::Parse(httpRequest.response.substr(start))[0][2].String())) if (auto translations = Copy(JSON::Parse(blob.value())[1][0].Array()))
{
std::wstring translation;
if (translations->size() == 1 && (translations = Copy(translations.value()[0][5].Array())))
if (translations->size() == 1)
{
for (const auto& sentence : translations.value()) if (sentence[0].String()) (translation += *sentence[0].String()) += L" ";
if (translations = Copy(translations.value()[0][5].Array()))
for (const auto& sentence : translations.value())
if (sentence[0].String()) (translation += *sentence[0].String()) += L" ";
}
else
{

View File

@ -47,7 +47,7 @@ public:
Window()
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
Localize();
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
if (scriptEditor.toPlainText().isEmpty()) scriptEditor.setPlainText(LUA_INTRO);
@ -57,6 +57,8 @@ public:
resize(800, 600);
setWindowTitle("Lua");
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
LoadScript();
}
~Window()

View File

@ -7,6 +7,7 @@ HttpRequest::HttpRequest(
const wchar_t* objectName,
std::string body,
const wchar_t* headers,
DWORD port,
const wchar_t* referrer,
DWORD requestFlags,
const wchar_t* httpVersion,
@ -16,7 +17,7 @@ HttpRequest::HttpRequest(
static std::atomic<HINTERNET> internet = NULL;
if (!internet) internet = WinHttpOpen(agentName, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
if (internet)
if (InternetHandle connection = WinHttpConnect(internet, serverName, INTERNET_DEFAULT_PORT, 0))
if (InternetHandle connection = WinHttpConnect(internet, serverName, port, 0))
if (InternetHandle request = WinHttpOpenRequest(connection, action, objectName, httpVersion, referrer, acceptTypes, requestFlags))
if (WinHttpSendRequest(request, headers, -1UL, body.empty() ? NULL : body.data(), body.size(), body.size(), NULL))
{

View File

@ -14,6 +14,7 @@ struct HttpRequest
const wchar_t* objectName,
std::string body = "",
const wchar_t* headers = NULL,
DWORD port = INTERNET_DEFAULT_PORT,
const wchar_t* referrer = NULL,
DWORD requestFlags = WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_DISABLE,
const wchar_t* httpVersion = NULL,
@ -37,7 +38,7 @@ namespace JSON
std::basic_string<C> Escape(std::basic_string<C> text)
{
int oldSize = text.size();
text.resize(text.size() + std::count_if(text.begin(), text.end(), [](auto ch) { return ch == '\n' || ch == '\r' || ch == '\t' || ch == '\\' || ch == '"'; }));
text.resize(text.size() + std::count_if(text.begin(), text.end(), [](C ch) { return ch == '\n' || ch == '\r' || ch == '\t' || ch == '\\' || ch == '"'; }));
auto out = text.rbegin();
for (int i = oldSize - 1; i >= 0; --i)
{
@ -59,13 +60,13 @@ namespace JSON
template <typename C> struct UTF {};
template <> struct UTF<wchar_t>
{
inline static std::wstring FromCodepoint(int codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs
inline static std::wstring FromCodepoint(unsigned codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs
};
template <typename C>
struct Value : private std::variant<std::monostate, std::nullopt_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>
struct Value : private std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>
{
using std::variant<std::monostate, std::nullopt_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>::variant;
using std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>::variant;
explicit operator bool() const { return index(); }
bool IsNull() const { return index() == 1; }
@ -77,17 +78,18 @@ namespace JSON
const Value<C>& operator[](std::basic_string<C> key) const
{
static const Value<C> failure;
if (auto object = Object()) if (auto it = object->find(key); it != object->end()) return it->second;
return failure;
}
const Value<C>& operator[](int i) const
{
static const Value<C> failure;
if (auto array = Array()) if (i < array->size()) return array->at(i);
return failure;
}
static const Value<C> failure;
};
template <typename C> const Value<C> Value<C>::failure;
template <typename C, int maxDepth = 25>
Value<C> Parse(const std::basic_string<C>& text, int64_t& i, int depth)
@ -115,7 +117,7 @@ namespace JSON
if (ch == 'u' && isxdigit(text[i + 2]) && isxdigit(text[i + 3]) && isxdigit(text[i + 4]) && isxdigit(text[i + 5]))
{
char charCode[] = { text[i + 2], text[i + 3], text[i + 4], text[i + 5], 0 };
unescaped += UTF<C>::FromCodepoint(strtol(charCode, nullptr, 16));
unescaped += UTF<C>::FromCodepoint(strtoul(charCode, nullptr, 16));
i += 5;
continue;
}
@ -136,7 +138,7 @@ namespace JSON
static C nullStr[] = { 'n', 'u', 'l', 'l' }, trueStr[] = { 't', 'r', 'u', 'e' }, falseStr[] = { 'f', 'a', 'l', 's', 'e' };
if (ch == nullStr[0])
if (std::char_traits<C>::compare(text.data() + i, nullStr, std::size(nullStr)) == 0) return i += std::size(nullStr), std::nullopt;
if (std::char_traits<C>::compare(text.data() + i, nullStr, std::size(nullStr)) == 0) return i += std::size(nullStr), nullptr;
else return {};
if (ch == trueStr[0])
if (std::char_traits<C>::compare(text.data() + i, trueStr, std::size(trueStr)) == 0) return i += std::size(trueStr), true;

View File

@ -21,7 +21,7 @@ public:
Window()
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
Localize();
ui.setupUi(this);
connect(ui.input, &QLineEdit::textEdited, this, &Window::setRegex);
@ -34,7 +34,7 @@ public:
void setRegex(QString regex)
{
ui.input->setText(regex);
std::lock_guard l(m);
std::scoped_lock lock(m);
if (!regex.isEmpty()) try { ::regex = S(regex); }
catch (std::regex_error) { return ui.output->setText(INVALID_REGEX); }
else ::regex = std::nullopt;

View File

@ -59,14 +59,12 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
std::wstring substring(sentence, suffixArray[i], commonPrefixLength);
bool substringCharMap[0x10000] = {};
for (auto ch : substring)
substringCharMap[ch] = true;
for (auto ch : substring) substringCharMap[ch] = true;
for (int regionSize = 0, j = 0; j <= sentence.size(); ++j)
if (substringCharMap[sentence[j]]) regionSize += 1;
else if (regionSize >= commonPrefixLength * 2)
while (regionSize > 0)
sentence[j - regionSize--] = ERASED;
while (regionSize > 0) sentence[j - regionSize--] = ERASED;
else regionSize = 0;
if (!wcsstr(sentence.c_str(), substring.c_str())) std::copy(substring.begin(), substring.end(), sentence.begin() + max(suffixArray[i], suffixArray[i + 1]));

View File

@ -87,7 +87,7 @@ void UpdateReplacements()
try
{
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
std::scoped_lock l(m);
std::scoped_lock lock(m);
trie = Trie(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary));
}
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
@ -103,7 +103,8 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
if (trie.Empty())
{
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
for (auto ch : std::wstring_view(REPLACER_INSTRUCTIONS)) file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
for (auto ch : std::wstring_view(REPLACER_INSTRUCTIONS))
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
_spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); // show file to user
}
}

View File

@ -17,7 +17,7 @@ public:
Window()
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
Localize();
connect(&linkButton, &QPushButton::clicked, this, &Window::Link);
layout.addWidget(&linkList);
@ -35,7 +35,7 @@ private:
int to = QInputDialog::getText(this, THREAD_LINK_TO, HEXADECIMAL, QLineEdit::Normal, "", &ok3, Qt::WindowCloseButtonHint).toInt(&ok4, 16);
if (ok1 && ok2 && ok3 && ok4)
{
std::lock_guard l(m);
std::scoped_lock lock(m);
linkedTextHandles[from].insert(to);
linkList.addItem(QString::number(from, 16) + "->" + QString::number(to, 16));
}
@ -47,7 +47,7 @@ private:
{
QStringList link = linkList.currentItem()->text().split("->");
linkList.takeItem(linkList.currentRow());
std::lock_guard l(m);
std::scoped_lock lock(m);
linkedTextHandles[link[0].toInt(nullptr, 16)].erase(link[1].toInt(nullptr, 16));
}
}

View File

@ -32,16 +32,18 @@ QFormLayout* display;
Settings settings;
Synchronized<std::wstring> translateTo = L"en", apiKey;
Synchronized<std::map<std::wstring, std::wstring>> translationCache;
int savedSize;
void SaveCache()
namespace
{
Synchronized<std::map<std::wstring, std::wstring>> translationCache;
int savedSize;
void SaveCache()
{
std::wstring allTranslations(L"\xfeff");
for (const auto& [sentence, translation] : translationCache.Acquire().contents)
allTranslations.append(L"|SENTENCE|").append(sentence).append(L"|TRANSLATION|").append(translation).append(L"|END|\r\n");
std::ofstream(TRANSLATION_CACHE_FILE, std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
savedSize = translationCache->size();
}
}
class Window : public QDialog
@ -50,7 +52,7 @@ public:
Window() :
QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
Localize();
display = new QFormLayout(this);
settings.beginGroup(TRANSLATION_PROVIDER);
@ -93,7 +95,7 @@ public:
}
if (GET_API_KEY_FROM)
{
auto keyInput = new QLineEdit(settings.value(API_KEY).toString());
auto keyInput = new QLineEdit(settings.value(API_KEY).toString(), this);
apiKey->assign(S(keyInput->text()));
QObject::connect(keyInput, &QLineEdit::textChanged, [](QString key) { settings.setValue(API_KEY, S(apiKey->assign(S(key)))); });
auto keyLabel = new QLabel(QString("<a href=\"%1\">%2</a>").arg(GET_API_KEY_FROM, API_KEY), this);
@ -147,8 +149,15 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
Synchronized<std::vector<DWORD>> tokens;
} rateLimiter;
auto StripWhitespace = [](std::wstring& text)
{
text.erase(text.begin(), std::find_if_not(text.begin(), text.end(), iswspace));
text.erase(std::find_if_not(text.rbegin(), text.rend(), iswspace).base(), text.end());
};
bool cache = false;
std::wstring translation;
StripWhitespace(sentence);
if (useCache)
{
auto translationCache = ::translationCache.Acquire();
@ -157,6 +166,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
if (rateLimiter.Request() || !rateLimitAll || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence);
else translation = TOO_MANY_TRANS_REQUESTS;
StripWhitespace(translation);
if (cache) translationCache->try_emplace(sentence, translation);
if (cache && translationCache->size() > savedSize + 50) SaveCache();

View File

@ -82,6 +82,8 @@ static struct // should be inline but MSVC (linker) is bugged
template <typename T> operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
} DUMMY;
inline auto Swallow = [](auto&&...) {};
template <typename T> std::optional<std::remove_cv_t<T>> Copy(T* ptr) { if (ptr) return *ptr; return {}; }
template <typename T> inline auto FormatArg(T arg) { return arg; }
@ -134,7 +136,7 @@ inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { Mes
template <typename... Args>
inline void TEXTRACTOR_DEBUG(const wchar_t* format, const Args&... args) { std::thread([=] { TEXTRACTOR_MESSAGE(format, args...); }).detach(); }
void localize();
void Localize();
#ifdef _DEBUG
#define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr);

View File

@ -22,7 +22,7 @@
const char* NATIVE_LANGUAGE = "English";
const char* ATTACH = u8"Attach to game";
const char* LAUNCH = u8"Launch game";
const char* GAME_CONFIG = u8"Configure game";
const char* CONFIG = u8"Configure game";
const char* DETACH = u8"Detach from game";
const char* FORGET = u8"Forget game";
const char* ADD_HOOK = u8"Add hook";
@ -144,6 +144,13 @@ const wchar_t* TOO_MANY_TRANS_REQUESTS = L"Rate limit exceeded: refuse to make m
const wchar_t* TRANSLATION_ERROR = L"Error while translating";
const char* USE_PREV_SENTENCE_CONTEXT = u8"Use previous sentence as context";
const char* API_KEY = u8"API key";
const char* CHROME_LOCATION = "Google Chrome location";
const char* START_DEVTOOLS = u8"Start DevTools";
const char* STOP_DEVTOOLS = u8"Stop DevTools";
const char* HEADLESS_MODE = u8"Headless mode";
const char* DEVTOOLS_STATUS = u8"DevTools status";
const char* AUTO_START = u8"Start automatically";
const wchar_t* ERROR_START_CHROME = L"failed to start Chrome or to connect to it";
const char* EXTRA_WINDOW_INFO = u8R"(Right click to change settings
Click and drag on window edges to move, or the bottom right corner to resize)";
const char* SENTENCE_TOO_BIG = u8"Sentence too large to display";
@ -215,7 +222,7 @@ const char* THREAD_LINK_FROM = u8"Thread number to link from";
const char* THREAD_LINK_TO = u8"Thread number to link to";
const char* HEXADECIMAL = u8"Hexadecimal";
void localize()
void Localize()
{
#ifdef TURKISH
NATIVE_LANGUAGE = "Turkish";
@ -316,7 +323,7 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
NATIVE_LANGUAGE = "Chinese (simplified)";
ATTACH = u8"附加到游戏";
LAUNCH = u8"启动游戏";
GAME_CONFIG = u8"配置游戏";
CONFIG = u8"配置游戏";
DETACH = u8"从游戏分离";
FORGET = u8"移除游戏";
ADD_HOOK = u8"添加钩子";
@ -469,7 +476,7 @@ end)";
NATIVE_LANGUAGE = "Russian";
ATTACH = u8"Присоединить к игре";
LAUNCH = u8"Запустить игру";
GAME_CONFIG = u8"Настройки игры";
CONFIG = u8"Настройки игры";
DETACH = u8"Отсоединить от игры";
FORGET = u8"Забыть игру";
ADD_HOOK = u8"Добавить хук";
@ -725,7 +732,7 @@ Klik dan tarik pinggiran jendela untuk memindahkan, atau sudut kanan bawah untuk
NATIVE_LANGUAGE = "Italian";
ATTACH = u8"Collega al gioco";
LAUNCH = u8"Avvia gioco";
GAME_CONFIG = u8"Configura gioco";
CONFIG = u8"Configura gioco";
DETACH = u8"Scollega dal gioco";
FORGET = u8"Dimentica gioco";
ADD_HOOK = u8"Aggiungi gancio";
@ -1145,7 +1152,7 @@ original_text의 빈공간은 무시되지만, replacement_text는 공백과 엔
NATIVE_LANGUAGE = "French";
ATTACH = u8"Attacher le jeu";
LAUNCH = u8"Lancer le jeu";
GAME_CONFIG = u8"Configure le jeu";
CONFIG = u8"Configure le jeu";
DETACH = u8"Detacher du jeu";
FORGET = u8"Oublier le jeu";
ADD_HOOK = u8"Ajouter un hook";
@ -1336,4 +1343,4 @@ Ce fichier doit être encodé en Unicode (UTF-16 Little Endian).)";
#endif // FRENCH
};
static auto _ = (localize(), 0);
static auto _ = (Localize(), 0);

View File

@ -35,7 +35,7 @@ namespace Engine
void Hijack()
{
static auto _ = []
static auto _ = ([]
{
GetModuleFileNameW(nullptr, processPath, MAX_PATH);
processName = wcsrchr(processPath, L'\\') + 1;
@ -68,8 +68,7 @@ namespace Engine
ConsoleOutput("Textractor: hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress);
DetermineEngineType();
return NULL;
}();
}(), 0);
}
bool ShouldMonoHook(const char* name)

View File

@ -86,7 +86,7 @@ namespace { // unnamed
0x5b, // pop rbx
0x58, // pop rax
0x9d, // pop rflags
0xff, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp qword ptr [0] ; relative to next instruction (i.e. jmp @original)
0xff, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp qword ptr [rip] ; relative to next instruction (i.e. jmp @original)
0,0,0,0,0,0,0,0 // @original
};
int this_offset = 50, send_offset = 60, original_offset = 126;