diff --git a/CREDITS.md b/CREDITS.md index 0639c88..a384875 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -6,10 +6,11 @@ If you're on this list and want your link changed let me know. - [Niakr1s](https://github.com/Niakr1s) - [tinyAdapter](https://github.com/tinyAdapter) - [lgztx96](https://github.com/lgztx96) - - with a special thanks to everyone reporting issues :) -- French translation by Racky + - [Jazzinghen](https://github.com/Jazzinghen) + - [luojunyuan](https://github.com/luojunyuan) +- French translation by Racky and [Gratusfr](https://github.com/Gratusfr) - Spanish translation by [scese250](https://github.com/scese250) -- Turkish translation by niisokusu +- Turkish translation by [niisokusu](https://reddit.com/u/niisokusu) - Simplified Chinese translation by [tinyAdapter](https://github.com/tinyAdapter) and [lgztx96](https://github.com/lgztx96) - Russian translation by [TokcDK](https://github.com/TokcDK) - Indonesian translation by [Hawxone](https://github.com/Hawxone) @@ -19,8 +20,9 @@ If you're on this list and want your link changed let me know. - Italian translation by [StarFang208](https://github.com/StarFang208) - ITHVNR updated by [mireado](https://github.com/mireado), [Eguni](https://github.com/Eguni), and [IJEMIN](https://github.com/IJEMIN) - ITHVNR originally made by [Stomp](http://www.hongfire.com/forum/member/325894-stomp) -- VNR engine made by [jichi](https://archive.is/prJwr) +- VNR engine made by [jichi](https://github.com/jichifly) - ITH updated by [Andys](https://github.com/AndyScull) -- ITH originally made by [kaosu](http://www.hongfire.com/forum/member/562651-kaosu) +- ITH originally made by [kaosu](https://code.google.com/archive/p/interactive-text-hooker) - Locale Emulator library made by [xupefei](https://github.com/xupefei) - MinHook library made by [TsudaKageyu](https://github.com/TsudaKageyu) +- Last but not least, the many people that have reported issues. To everybody mentioned here: Thank You! diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index 303df32..5cfb22c 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -147,7 +147,7 @@ namespace { QMultiHash allProcesses; for (auto [processId, processName] : GetAllProcesses()) - if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::wstring::npos)) + if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::string::npos)) allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId); QStringList processList(allProcesses.uniqueKeys()); @@ -165,7 +165,7 @@ namespace std::wstring path = std::wstring(process).erase(process.rfind(L'\\')); PROCESS_INFORMATION info = {}; - auto useLocale = openSettings().value(CONFIG_JP_LOCALE, PROMPT).toInt(); + auto useLocale = Settings().value(CONFIG_JP_LOCALE, PROMPT).toInt(); if (!x64 && (useLocale == ALWAYS || (useLocale == PROMPT && QMessageBox::question(This, SELECT_PROCESS, USE_JP_LOCALE) == QMessageBox::Yes))) { if (HMODULE localeEmulator = LoadLibraryW(L"LoaderDll")) @@ -202,7 +202,7 @@ namespace { if (auto processName = GetModuleFilename(selectedProcessId)) if (int last = processName->rfind(L'\\') + 1) { - std::wstring configFile = std::wstring(processName.value()).replace(last, std::wstring::npos, GAME_CONFIG_FILE); + 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))); @@ -439,10 +439,10 @@ namespace }).detach(); } - void Settings() + void OpenSettings() { QDialog dialog(This, Qt::WindowCloseButtonHint); - QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog); + Settings settings(&dialog); QFormLayout layout(&dialog); QPushButton saveButton(SAVE_SETTINGS, &dialog); for (auto [value, label] : Array{ @@ -501,7 +501,7 @@ namespace font.fromString(fontString); font.setStyleStrategy(QFont::NoFontMerging); ui.textOutput->setFont(font); - QSettings(CONFIG_FILE, QSettings::IniFormat).setValue(FONT, font.toString()); + Settings().setValue(FONT, font.toString()); } void ProcessConnected(DWORD processId) @@ -605,7 +605,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { REMOVE_HOOKS, RemoveHooks }, { SAVE_HOOKS, SaveHooks }, { SEARCH_FOR_HOOKS, FindHooks }, - { SETTINGS, Settings }, + { SETTINGS, OpenSettings }, { EXTENSIONS, Extensions } }) { @@ -623,7 +623,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) connect(ui.textOutput, &QPlainTextEdit::selectionChanged, this, CopyUnlessMouseDown); connect(ui.textOutput, &QPlainTextEdit::customContextMenuRequested, this, OutputContextMenu); - QSettings settings(CONFIG_FILE, QSettings::IniFormat); + Settings settings; 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()); TextThread::filterRepetition = settings.value(FILTER_REPETITION, TextThread::filterRepetition).toBool(); @@ -649,7 +649,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) if (arg[1] == L'p' || arg[1] == L'P') if (DWORD processId = _wtoi(arg.substr(2).c_str())) Host::InjectProcess(processId); else for (auto [processId, processName] : processes) - if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::wstring::npos) Host::InjectProcess(processId); + if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::string::npos) Host::InjectProcess(processId); std::thread([] { @@ -672,7 +672,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) MainWindow::~MainWindow() { - openSettings().setValue(WINDOW, geometry()); + Settings().setValue(WINDOW, geometry()); CleanupExtensions(); SetErrorMode(SEM_NOGPFAULTERRORBOX); ExitProcess(0); diff --git a/extensions/bingtranslate.cpp b/extensions/bingtranslate.cpp index 9310833..cb0983f 100644 --- a/extensions/bingtranslate.cpp +++ b/extensions/bingtranslate.cpp @@ -91,12 +91,11 @@ std::pair Translate(const std::wstring& text) L"api.cognitive.microsofttranslator.com", L"POST", FormatString(L"/translate?api-version=3.0&to=%s", translateTo.Copy()).c_str(), - FormatString(R"([{"text":"%s"}])", JSON::Escape(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() }) { - // Response formatted as JSON: translation starts with text":" and ends with ","to - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"text\":\"(.+?)\",\""))) return { true, results[1] }; + 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) }; @@ -107,8 +106,7 @@ std::pair Translate(const std::wstring& text) L"POST", FormatString(L"/ttranslatev3?fromLang=auto-detect&to=%s&text=%s", translateTo.Copy(), Escape(text)).c_str() }) - // Response formatted as JSON: translation starts with text":" and ends with ","to - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"text\":\"(.+?)\",\""))) return { true, results[1] }; + 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) }; } diff --git a/extensions/deepltranslate.cpp b/extensions/deepltranslate.cpp index ddc0e80..831d9ad 100644 --- a/extensions/deepltranslate.cpp +++ b/extensions/deepltranslate.cpp @@ -50,13 +50,14 @@ std::pair Translate(const std::wstring& text) L"Content-Type: application/x-www-form-urlencoded" }))) // Response formatted as JSON: translation starts with text":" and ends with "}] - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"text\":\"(.+?)\"\\}\\]"))) return { true, results[1] }; + if (auto translation = Copy(JSON::Parse(httpRequest.response)[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) }; // the following code was reverse engineered from the DeepL website; it's as close as I could make it but I'm not sure what parts of this could be removed and still have it work int64_t r = _time64(nullptr), n = std::count(text.begin(), text.end(), L'i') + 1; - int id = 10000 * std::uniform_int_distribution(0, 9999)(std::mt19937(std::random_device()())); + thread_local auto generator = std::mt19937(std::random_device()()); + int id = 10000 * std::uniform_int_distribution(0, 9999)(generator) + 1; // user_preferred_langs? what should priority be? does timestamp do anything? other translation quality options? auto body = FormatString(R"( { @@ -80,21 +81,19 @@ std::pair Translate(const std::wstring& text) }] } } - )", ++id, r + (n - r % n), translateTo.Copy(), JSON::Escape(text)); + )", id, r + (n - r % n), translateTo.Copy(), JSON::Escape(WideStringToString(text))); // missing accept-encoding header since it fucks up HttpRequest - std::wstring headers = L"Host: www2.deepl.com\r\nAccept-Language: en-US,en;q=0.5\r\nContent-type: text/plain; charset=utf-8\r\nOrigin: https://www.deepl.com\r\nTE: Trailers"; if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"www2.deepl.com", L"POST", L"/jsonrpc", body, - headers.c_str(), + 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", L"https://www.deepl.com/translator", WINHTTP_FLAG_SECURE }) - // Response formatted as JSON: translation starts with preprocessed_sentence":" and ends with "," - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"postprocessed_sentence\":\"(.+?)\",\""))) return { true, results[1] }; + if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"result"][L"translations"][0][L"beams"][0][L"postprocessed_sentence"].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) }; } diff --git a/extensions/extrawindow.cpp b/extensions/extrawindow.cpp index b2a581d..a602595 100644 --- a/extensions/extrawindow.cpp +++ b/extensions/extrawindow.cpp @@ -46,6 +46,7 @@ struct PrettyWindow : QDialog { PrettyWindow(const char* name) { + localize(); ui.setupUi(this); ui.display->setGraphicsEffect(&outliner); setWindowFlags(Qt::FramelessWindowHint); @@ -76,7 +77,7 @@ struct PrettyWindow : QDialog protected: QMenu menu{ ui.display }; - QSettings settings{ openSettings(this) }; + Settings settings{ this }; private: void RequestFont() @@ -196,7 +197,7 @@ public: void AddSentence(QString sentence) { if (sentence.size() > maxSentenceSize) sentence = SENTENCE_TOO_BIG; - if (!showOriginal) sentence = sentence.section('\n', sentence.count('\n') / 2 + 1); + if (!showOriginal && sentence.contains(u8"\x200b \n")) sentence = sentence.split(u8"\x200b \n")[1]; sanitize(sentence); sentence.chop(std::distance(std::remove(sentence.begin(), sentence.end(), QChar::Tabulation), sentence.end())); sentenceHistory.push_back(sentence); diff --git a/extensions/googletranslate.cpp b/extensions/googletranslate.cpp index e8ee228..95abfd2 100644 --- a/extensions/googletranslate.cpp +++ b/extensions/googletranslate.cpp @@ -125,37 +125,6 @@ QStringList languages bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true; int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 500; -unsigned TKK = 0; - -std::wstring GetTranslationUri(const std::wstring& text) -{ - // If no TKK available, use this uri. Can't use too much or google will detect unauthorized access - if (!TKK) return FormatString(L"/translate_a/single?client=gtx&dt=ld&dt=rm&dt=t&tl=%s&q=%s", translateTo.Copy(), Escape(text)); - - // reverse engineered from translate.google.com - std::wstring escapedText; - unsigned a = time(NULL) / 3600, b = a; // the first part of TKK - for (unsigned char ch : WideStringToString(text)) - { - escapedText += FormatString(L"%%%02X", (int)ch); - a += ch; - a += a << 10; - a ^= a >> 6; - } - a += a << 3; - a ^= a >> 11; - a += a << 15; - a ^= TKK; - a %= 1000000; - - return FormatString(L"/translate_a/single?client=webapp&dt=ld&dt=rm&dt=t&sl=auto&tl=%s&tk=%u.%u&q=%s", translateTo.Copy(), a, a ^ b, escapedText); -} - -bool IsHash(const std::wstring& result) -{ - return result.size() == 32 && std::all_of(result.begin(), result.end(), [](char ch) { return (ch >= L'0' && ch <= L'9') || (ch >= L'a' && ch <= L'z'); }); -} - std::pair Translate(const std::wstring& text) { if (!apiKey->empty()) @@ -164,31 +133,40 @@ std::pair Translate(const std::wstring& text) L"translation.googleapis.com", L"POST", FormatString(L"/language/translate/v2?format=text&target=%s&key=%s", translateTo.Copy(), apiKey.Copy()).c_str(), - FormatString(R"({"q":["%s"]})", JSON::Escape(text)) + FormatString(R"({"q":["%s"]})", JSON::Escape(WideStringToString(text))) }) - { - // Response formatted as JSON: starts with "translatedText": " and translation is enclosed in quotes followed by a comma - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"\"translatedText\": \"(.+?)\","))) return { true, results[1] }; - return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) }; - } + if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"data"][L"translations"][0][L"translatedText"].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 (!TKK) - if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"translate.google.com", L"GET", L"/" }) - if (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"(\\d{7,})'"))) - _InterlockedCompareExchange(&TKK, stoll(results[1]), 0); - - if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"translate.googleapis.com", L"GET", GetTranslationUri(text).c_str() }) + if (HttpRequest httpRequest{ + L"Mozilla/5.0 Textractor", + L"translate.google.com", + L"POST", + L"/_/TranslateWebserverUi/data/batchexecute?rpcids=MkEWBc", + "f.req=" + Escape(WideStringToString(FormatString(LR"([[["MkEWBc","[[\"%s\",\"auto\",\"%s\",true],[null]]",null,"generic"]]])", JSON::Escape((JSON::Escape(text))), translateTo.Copy()))), + L"Content-Type: application/x-www-form-urlencoded" + }) { - // Response formatted as JSON: starts with "[[[" and translation is enclosed in quotes followed by a comma - if (httpRequest.response[0] == L'[') + if (auto start = httpRequest.response.find(L"[["); start != std::string::npos) { - std::wstring translation; - for (std::wsmatch results; std::regex_search(httpRequest.response, results, std::wregex(L"\\[\"(.*?)\",[n\"]")); httpRequest.response = results.suffix()) - if (!IsHash(results[1])) translation += std::wstring(results[1]) + L" "; - if (!translation.empty()) return { true, translation }; + 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()))) + { + for (const auto& sentence : translations.value()) if (sentence[0].String()) (translation += *sentence[0].String()) += L" "; + } + else + { + for (const auto& conjugation : translations.value()) + if (auto sentence = conjugation[0].String()) if (auto gender = conjugation[2].String()) translation += FormatString(L"%s %s\n", *sentence, *gender); + } + if (!translation.empty()) return { true, translation }; + return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, blob.value()) }; + } } - return { false, FormatString(L"%s (TKK=%u): %s", TRANSLATION_ERROR, _InterlockedExchange(&TKK, 0), httpRequest.response) }; + return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) }; } else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) }; } diff --git a/extensions/lua.cpp b/extensions/lua.cpp index c374527..0999774 100644 --- a/extensions/lua.cpp +++ b/extensions/lua.cpp @@ -47,6 +47,7 @@ public: Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint) { + localize(); connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript); if (scriptEditor.toPlainText().isEmpty()) scriptEditor.setPlainText(LUA_INTRO); @@ -83,9 +84,9 @@ private: bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) { - thread_local static struct { std::unique_ptr> L{ luaL_newstate() }; operator lua_State*() { return L.get(); } } L; - thread_local static auto _ = (luaL_openlibs(L), luaL_dostring(L, "function ProcessSentence() end")); - thread_local static int revCount = 0; + thread_local struct { std::unique_ptr> L{ luaL_newstate() }; operator lua_State*() { return L.get(); } } L; + thread_local auto _ = (luaL_openlibs(L), luaL_dostring(L, "function ProcessSentence() end")); + thread_local int revCount = 0; if (::revCount > revCount) { diff --git a/extensions/network.cpp b/extensions/network.cpp index ca3cd76..8a3f99f 100644 --- a/extensions/network.cpp +++ b/extensions/network.cpp @@ -55,44 +55,11 @@ std::wstring Escape(const std::wstring& text) return escaped; } -namespace JSON +std::string Escape(const std::string& text) { - void Unescape(std::wstring& text) - { - for (int i = 0; i < text.size(); ++i) - { - if (text[i] == L'\\') - { - text[i] = 0; - if (text[i + 1] == L'r') text[i + 1] = 0; // for some reason \r gets displayed as a newline - if (text[i + 1] == L'n') text[i + 1] = L'\n'; - if (text[i + 1] == L't') text[i + 1] = L'\t'; - if (text[i + 1] == L'\\') ++i; - } - } - text.erase(std::remove(text.begin(), text.end(), 0), text.end()); - } - - std::string Escape(const std::wstring& text) - { - std::string escaped = WideStringToString(text); - int oldSize = escaped.size(); - escaped.resize(escaped.size() + std::count_if(escaped.begin(), escaped.end(), [](char ch) { return ch == '\n' || ch == '\r' || ch == '\t' || ch == '\\' || ch == '"'; })); - auto out = escaped.rbegin(); - for (int i = oldSize - 1; i >= 0; --i) - { - if (escaped[i] == '\n') *out++ = 'n'; - else if (escaped[i] == '\t') *out++ = 't'; - else if (escaped[i] == '\r') *out++ = 'r'; - else if (escaped[i] == '\\' || escaped[i] == '"') *out++ = escaped[i]; - else - { - *out++ = escaped[i]; - continue; - } - *out++ = '\\'; - } - escaped.erase(std::remove_if(escaped.begin(), escaped.end(), [](unsigned char ch) { return ch < 0x20; }), escaped.end()); - return escaped; - } + std::string escaped; + for (unsigned char ch : text) escaped += FormatString("%%%02X", (int)ch); + return escaped; } + +TEST(assert(JSON::Parse(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])"))) diff --git a/extensions/network.h b/extensions/network.h index 27ae6a4..045ab08 100644 --- a/extensions/network.h +++ b/extensions/network.h @@ -1,6 +1,7 @@ #pragma once #include +#include using InternetHandle = AutoHandle>; @@ -29,9 +30,173 @@ struct HttpRequest }; std::wstring Escape(const std::wstring& text); +std::string Escape(const std::string& text); namespace JSON { - void Unescape(std::wstring& text); - std::string Escape(const std::wstring& text); + template + std::basic_string Escape(std::basic_string 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 == '"'; })); + auto out = text.rbegin(); + for (int i = oldSize - 1; i >= 0; --i) + { + if (text[i] == '\n') *out++ = 'n'; + else if (text[i] == '\t') *out++ = 't'; + else if (text[i] == '\r') *out++ = 'r'; + else if (text[i] == '\\' || text[i] == '"') *out++ = text[i]; + else + { + *out++ = text[i]; + continue; + } + *out++ = '\\'; + } + text.erase(std::remove_if(text.begin(), text.end(), [](uint64_t ch) { return ch < 0x20 || ch == 0x7f; }), text.end()); + return text; + } + + template struct UTF {}; + template <> struct UTF + { + inline static std::wstring FromCodepoint(int codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs + }; + + template + struct Value : private std::variant, std::vector>, std::unordered_map, Value>> + { + using std::variant, std::vector>, std::unordered_map, Value>>::variant; + + explicit operator bool() const { return index(); } + bool IsNull() const { return index() == 1; } + auto Boolean() const { return std::get_if(this); } + auto Number() const { return std::get_if(this); } + auto String() const { return std::get_if>(this); } + auto Array() const { return std::get_if>>(this); } + auto Object() const { return std::get_if, Value>>(this); } + + const Value& operator[](std::basic_string key) const + { + static const Value failure; + if (auto object = Object()) if (auto it = object->find(key); it != object->end()) return it->second; + return failure; + } + const Value& operator[](int i) const + { + static const Value failure; + if (auto array = Array()) if (i < array->size()) return array->at(i); + return failure; + } + }; + + template + Value Parse(const std::basic_string& text, int64_t& i, int depth) + { + if (depth > maxDepth) return {}; + C ch; + auto SkipWhitespace = [&] + { + while (i < text.size() && (text[i] == ' ' || text[i] == '\n' || text[i] == '\r' || text[i] == '\t')) ++i; + if (i >= text.size()) return true; + ch = text[i]; + return false; + }; + auto ExtractString = [&] + { + std::basic_string unescaped; + i += 1; + for (; i < text.size(); ++i) + { + auto ch = text[i]; + if (ch == '"') return i += 1, unescaped; + if (ch == '\\') + { + ch = text[i + 1]; + 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::FromCodepoint(strtol(charCode, nullptr, 16)); + i += 5; + continue; + } + for (auto [original, value] : Array{ { 'b', '\b' }, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'} }) if (ch == original) + { + unescaped.push_back(value); + goto replaced; + } + unescaped.push_back(ch); + replaced: i += 1; + } + else unescaped.push_back(ch); + } + return unescaped; + }; + + if (SkipWhitespace()) return {}; + + 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::compare(text.data() + i, nullStr, std::size(nullStr)) == 0) return i += std::size(nullStr), std::nullopt; + else return {}; + if (ch == trueStr[0]) + if (std::char_traits::compare(text.data() + i, trueStr, std::size(trueStr)) == 0) return i += std::size(trueStr), true; + else return {}; + if (ch == falseStr[0]) + if (std::char_traits::compare(text.data() + i, falseStr, std::size(falseStr)) == 0) return i += std::size(falseStr), false; + else return {}; + + if (ch == '-' || (ch >= '0' && ch <= '9')) + { + std::string number; + for (; i < text.size() && ((text[i] >= '0' && text[i] <= '9') || text[i] == '-' || text[i] == '+' || text[i] == 'e' || text[i] == 'E' || text[i] == '.'); ++i) + number.push_back(text[i]); + return strtod(number.c_str(), NULL); + } + + if (ch == '"') return ExtractString(); + + if (ch == '[') + { + std::vector> array; + while (true) + { + i += 1; + if (SkipWhitespace()) return {}; + if (ch == ']') return i += 1, Value(array); + if (!array.emplace_back(Parse(text, i, depth + 1))) return {}; + if (SkipWhitespace()) return {}; + if (ch == ']') return i += 1, Value(array); + if (ch != ',') return {}; + } + } + + if (ch == '{') + { + std::unordered_map, Value> object; + while (true) + { + i += 1; + if (SkipWhitespace()) return {}; + if (ch == '}') return i += 1, Value(object); + if (ch != '"') return {}; + auto key = ExtractString(); + if (SkipWhitespace() || ch != ':') return {}; + i += 1; + if (!(object[std::move(key)] = Parse(text, i, depth + 1))) return {}; + if (SkipWhitespace()) return {}; + if (ch == '}') return i += 1, Value(object); + if (ch != ',') return {}; + } + } + + return {}; + } + + template + Value Parse(const std::basic_string& text) + { + int64_t start = 0; + return Parse(text, start, 0); + } } diff --git a/extensions/regexfilter.cpp b/extensions/regexfilter.cpp index d8814a9..e4178ba 100644 --- a/extensions/regexfilter.cpp +++ b/extensions/regexfilter.cpp @@ -21,6 +21,7 @@ public: Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint) { + localize(); ui.setupUi(this); connect(ui.input, &QLineEdit::textEdited, this, &Window::setRegex); diff --git a/extensions/threadlinker.cpp b/extensions/threadlinker.cpp index 1414536..ed7751a 100644 --- a/extensions/threadlinker.cpp +++ b/extensions/threadlinker.cpp @@ -17,6 +17,7 @@ public: Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint) { + localize(); connect(&linkButton, &QPushButton::clicked, this, &Window::Link); layout.addWidget(&linkList); diff --git a/extensions/translatewrapper.cpp b/extensions/translatewrapper.cpp index 1e5ed6c..dd4613d 100644 --- a/extensions/translatewrapper.cpp +++ b/extensions/translatewrapper.cpp @@ -26,10 +26,10 @@ extern int tokenCount, tokenRestoreDelay, maxSentenceSize; std::pair Translate(const std::wstring& text); const char* LANGUAGE = u8"Language"; -const std::string TRANSLATION_CACHE_FILE = FormatString("%s Cache.txt", TRANSLATION_PROVIDER); +const std::string TRANSLATION_CACHE_FILE = FormatString("%s Translation Cache.txt", TRANSLATION_PROVIDER); QFormLayout* display; -QSettings settings = openSettings(); +Settings settings; Synchronized translateTo = L"en", apiKey; Synchronized> translationCache; @@ -50,6 +50,7 @@ public: Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint) { + localize(); display = new QFormLayout(this); settings.beginGroup(TRANSLATION_PROVIDER); @@ -159,9 +160,9 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) if (cache) translationCache->try_emplace(sentence, translation); if (cache && translationCache->size() > savedSize + 50) SaveCache(); - JSON::Unescape(translation); - sentence += L"\n" + translation; + for (int i = 0; i < translation.size(); ++i) if (translation[i] == '\r' && translation[i + 1] == '\n') translation[i] = 0x200b; // for some reason \r appears as newline - no need to double + if (!translation.empty()) (sentence += L"\x200b \n") += translation; return true; } -TEST(assert(Translate(L"こんにちは").second.find(L"ello") != std::wstring::npos)); +TEST(assert(Translate(L"こんにちは").second.find(L"ello") != std::string::npos)); diff --git a/include/common.h b/include/common.h index 13803bf..e316111 100644 --- a/include/common.h +++ b/include/common.h @@ -27,23 +27,15 @@ constexpr bool x64 = true; constexpr bool x64 = false; #endif -template -struct ArrayImpl { using Type = std::tuple[]; }; -template -struct ArrayImpl { using Type = T[]; }; -template -using Array = typename ArrayImpl::Type; +template struct ArrayImpl { using Type = std::tuple[]; }; +template struct ArrayImpl { using Type = T[]; }; +template using Array = typename ArrayImpl::Type; -template -using Functor = std::integral_constant, F>; - -template -struct Identity { V operator()(V v) const { return v; } }; +template using Functor = std::integral_constant, F>; // shouldn't need remove_reference_t but MSVC is bugged struct PermissivePointer { - template - operator T*() { return (T*)p; } + template operator T*() { return (T*)p; } void* p; }; @@ -77,29 +69,23 @@ public: Locker Acquire() { return { std::unique_lock(m), contents }; } Locker operator->() { return Acquire(); } - - T Copy() - { - return Acquire().contents; - } + T Copy() { return Acquire().contents; } private: T contents; M m; }; -static struct +static struct // should be inline but MSVC (linker) is bugged { - BYTE DUMMY[100]; - template - operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; } + inline static BYTE DUMMY[100]; + template operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; } } DUMMY; -template -inline auto FormatArg(T arg) { return arg; } +template std::optional> Copy(T* ptr) { if (ptr) return *ptr; return {}; } -template -inline auto FormatArg(const std::basic_string& arg) { return arg.c_str(); } +template inline auto FormatArg(T arg) { return arg; } +template inline auto FormatArg(const std::basic_string& arg) { return arg.c_str(); } #pragma warning(push) #pragma warning(disable: 4996) @@ -148,6 +134,8 @@ inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { Mes template inline void TEXTRACTOR_DEBUG(const wchar_t* format, const Args&... args) { std::thread([=] { TEXTRACTOR_MESSAGE(format, args...); }).detach(); } +void localize(); + #ifdef _DEBUG #define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr); #else diff --git a/include/qtcommon.h b/include/qtcommon.h index f0278cd..6cd3d91 100644 --- a/include/qtcommon.h +++ b/include/qtcommon.h @@ -23,8 +23,8 @@ static thread_local bool ok; constexpr auto CONFIG_FILE = u8"Textractor.ini"; constexpr auto WINDOW = u8"Window"; -inline QSettings openSettings(QObject* parent = nullptr) { return { CONFIG_FILE, QSettings::IniFormat, parent }; } +struct Settings : QSettings { Settings(QObject* parent = nullptr) : QSettings(CONFIG_FILE, QSettings::IniFormat, parent) {} }; struct QTextFile : QFile { QTextFile(QString name, QIODevice::OpenMode mode) : QFile(name) { open(mode | QIODevice::Text); } }; inline std::wstring S(const QString& s) { return { s.toStdWString() }; } inline QString S(const std::string& s) { return QString::fromStdString(s); } diff --git a/text.cpp b/text.cpp index 4dd7a7f..fe65c5a 100644 --- a/text.cpp +++ b/text.cpp @@ -215,7 +215,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"; -static auto _ = [] +void localize() { #ifdef TURKISH NATIVE_LANGUAGE = "Turkish"; @@ -1158,26 +1158,26 @@ original_text의 빈공간은 무시되지만, replacement_text는 공백과 엔 ATTACH_INFO = u8R"(Si vous ne voyez pas le processus que vous souhaitez joindre, essayez de l'exécuter avec les droits d'administrateur Vous pouvez également saisir l'ID de processus)"; SELECT_PROCESS_INFO = u8"Si vous saisissez manuellement le nom du fichier de processus, veuillez utiliser le chemin exact"; - FROM_COMPUTER = u8"Selectionner depuis l'ordinateur"; + FROM_COMPUTER = u8"Sélectionner depuis l'ordinateur"; PROCESSES = u8"Processus (*.exe)"; - CODE_INFODUMP = u8R"(Enter read code + CODE_INFODUMP = u8R"(Entrez le read code R{S|Q|V|M}[null_length<][codepage#]@addr -OR -Enter hook code +OU +Entrez le hook code H{A|B|W|H|S|Q|V|M}[F][null_length<][N][codepage#][padding+]data_offset[*deref_offset][:split_offset[*deref_offset]]@addr[:module[:func]] -All numbers except codepage/null_length in hexadecimal -Default codepage is 932 (Shift-JIS) but this can be changed in settings +Tous les nombres sauf codepage/null_length sont en hexadécimal +Le codepage par défaut est 932 (Shift-JIS) mais cela peut être modifié dans les paramètres A/B: codepage char little/big endian W: UTF-16 char H: Two hex bytes S/Q/V/M: codepage/UTF-16/UTF-8/hex string F: treat strings as full lines of text -N: don't use context +N: n'utilise pas de contexte null_length: length of null terminator used for string padding: length of padding data before string (C struct { int64_t size; char string[500]; } needs padding = 8) -Negatives for data_offset/split_offset refer to registers --4 for EAX, -8 for ECX, -C for EDX, -10 for EBX, -14 for ESP, -18 for EBP, -1C for ESI, -20 for EDI --C for RAX, -14 for RBX, -1C for RCX, -24 for RDX, and so on for RSP, RBP, RSI, RDI, R8-R15 +Les valeures négatives pour data_offset/split_offset font références aux registres +-4 pour EAX, -8 pour ECX, -C pour EDX, -10 pour EBX, -14 pour ESP, -18 pour EBP, -1C pour ESI, -20 pour EDI +-C pour RAX, -14 pour RBX, -1C pour RCX, -24 pour RDX, and so on for RSP, RBP, RSI, RDI, R8-R15 * means dereference pointer+deref_offset)"; SAVE_SETTINGS = u8"Sauvergarder les paramètres"; EXTEN_WINDOW_INSTRUCTIONS = u8R"(Pour ajouter une extension, cliquez avec le bouton droit sur la liste des extensions @@ -1192,26 +1192,26 @@ Pour supprimer une extension, sélectionnez-la et appuyez sur supprimer)"; USE_JP_LOCALE = u8"Émuler les paramètres régionaux japonais?"; FAILED_TO_CREATE_CONFIG_FILE = u8"Impossible de créer le fichier de configuration \"%1\""; HOOK_SEARCH_UNSTABLE_WARNING = u8"La recherche de crochets est instable! Soyez prêt à ce que votre jeu plante!"; - SEARCH_CJK = u8"Recher pour Chinois/Japonais/Coréen"; + SEARCH_CJK = u8"Rechercher pour Chinois/Japonais/Coréen"; SEARCH_PATTERN = u8"Modèle de recherche (tableau d'octets hexadécimaux)"; - SEARCH_DURATION = u8"Durée de la recherche(ms)"; + SEARCH_DURATION = u8"Durée de la recherche (ms)"; SEARCH_MODULE = u8"Recherche sans module"; PATTERN_OFFSET = u8"Décalage par rapport au début du modèle"; MAX_HOOK_SEARCH_RECORDS = u8"Limite du résultat de la recherche"; MIN_ADDRESS = u8"Minimum d'adresses (hex)"; MAX_ADDRESS = u8"Maximum d'adresses (hex)"; STRING_OFFSET = u8"Décalage de la chaîne (hex)"; - HOOK_SEARCH_FILTER = u8"Results must match this regex"; + HOOK_SEARCH_FILTER = u8"Les résultats doivent correspondre à ce regex"; TEXT = u8"Texte"; CODEPAGE = u8"Code de page"; SEARCH_FOR_TEXT = u8"Rechercher un texte spécifique"; START_HOOK_SEARCH = u8"Lancer la recherche de hook"; SAVE_SEARCH_RESULTS = u8"Sauvergarder les résultats de la recherche"; TEXT_FILES = u8"Texte (*.txt)"; - DOUBLE_CLICK_TO_REMOVE_HOOK = u8"Double click un hook pour l'enlever"; + DOUBLE_CLICK_TO_REMOVE_HOOK = u8"Double cliquer sur un hook pour l'enlever"; FILTER_REPETITION = u8"Répétition de filtre"; AUTO_ATTACH = u8"Attachement Automatique"; - ATTACH_SAVED_ONLY = u8"Attachement Automatique(Sauvergardé seulement)"; + ATTACH_SAVED_ONLY = u8"Attachement Automatique (Sauvergardé seulement)"; SHOW_SYSTEM_PROCESSES = u8"Montrer les processus système"; DEFAULT_CODEPAGE = u8"Page de code de base"; FLUSH_DELAY = u8"Retard de vidage"; @@ -1293,8 +1293,8 @@ Ce fichier doit être encodé en UTF-8.)"; Fonctionne uniquement si cette extension est utilisée directement après une extension de traduction)"; SIZE_LOCK = u8"Verouiller la taille"; OPACITY = u8"Opacité"; - BG_COLOR = u8"COuleur d'arrière-plan"; - TEXT_COLOR = u8"COuleur du texte"; + BG_COLOR = u8"Couleur d'arrière-plan"; + TEXT_COLOR = u8"Couleur du texte"; TEXT_OUTLINE = u8"Contour du texte"; OUTLINE_COLOR = u8"Couleur du contour"; OUTLINE_SIZE = u8"Taille du contour"; @@ -1332,8 +1332,8 @@ Ce fichier doit être encodé en Unicode (UTF-16 Little Endian).)"; LINK = u8"Lien"; THREAD_LINK_FROM = u8"Nombre du thread du lien depuis"; THREAD_LINK_TO = u8"Nombre du thread du lien a"; - HEXADECIMAL = u8"Hexadecimal"; + HEXADECIMAL = u8"Hexadécimal"; #endif // FRENCH +}; - return 0; -}(); +static auto _ = (localize(), 0); diff --git a/texthook/engine/engine.cc b/texthook/engine/engine.cc index 12713c8..8a12f0e 100644 --- a/texthook/engine/engine.cc +++ b/texthook/engine/engine.cc @@ -16396,19 +16396,19 @@ bool InsertShinyDaysGameHook() 0xff,0x83,0x70,0x03,0x00,0x00,0x33,0xf6, 0xc6,0x84,0x24,0x90,0x02,0x00,0x00,0x02 }; - LPVOID addr = (LPVOID)0x42ad94; - if (::memcmp(addr, bytes, sizeof(bytes)) != 0) { - ConsoleOutput("vnreng:ShinyDays: only work for 1.00"); - return false; + + for (auto addr : Util::SearchMemory(bytes, sizeof(bytes))) { + HookParam hp = {}; + hp.address = addr + 0x8; + hp.text_fun = SpecialGameHookShinyDays; + hp.type = USING_UNICODE | USING_STRING | NO_CONTEXT; + ConsoleOutput("Textractor: INSERT ShinyDays"); + NewHook(hp, "ShinyDays"); + return true; } - HookParam hp = {}; - hp.address = 0x42ad9c; - hp.text_fun = SpecialGameHookShinyDays; - hp.type = USING_UNICODE|USING_STRING|NO_CONTEXT; - ConsoleOutput("vnreng: INSERT ShinyDays"); - NewHook(hp, "ShinyDays 1.00"); - return true; + ConsoleOutput("Textractor:ShinyDays: pattern not found"); + return false; } #if 0 // disabled as lova does not allow module from being modified