Merge branch 'master' into pr/416-add-devtools-api-and-new-deepl-extension

This commit is contained in:
Akash Mozumdar 2020-12-16 12:23:52 -07:00
commit 3a34f989e5
16 changed files with 289 additions and 187 deletions

View File

@ -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!

View File

@ -147,7 +147,7 @@ namespace
{
QMultiHash<QString, DWORD> 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<bool&, const char*>{
@ -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);

View File

@ -91,12 +91,11 @@ std::pair<bool, std::wstring> 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<bool, std::wstring> 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) };
}

View File

@ -50,13 +50,14 @@ std::pair<bool, std::wstring> 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<bool, std::wstring> 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) };
}

View File

@ -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);

View File

@ -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<bool, std::wstring> Translate(const std::wstring& text)
{
if (!apiKey->empty())
@ -164,31 +133,40 @@ std::pair<bool, std::wstring> 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) };
}

View File

@ -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<lua_State, Functor<lua_close>> 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<lua_State, Functor<lua_close>> 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)
{

View File

@ -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<wchar_t>(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])")))

View File

@ -1,6 +1,7 @@
#pragma once
#include <winhttp.h>
#include <variant>
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
@ -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 <typename C>
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 == '"'; }));
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 <typename C> struct UTF {};
template <> struct UTF<wchar_t>
{
inline static std::wstring FromCodepoint(int 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>>>
{
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;
explicit operator bool() const { return index(); }
bool IsNull() const { return index() == 1; }
auto Boolean() const { return std::get_if<bool>(this); }
auto Number() const { return std::get_if<double>(this); }
auto String() const { return std::get_if<std::basic_string<C>>(this); }
auto Array() const { return std::get_if<std::vector<Value<C>>>(this); }
auto Object() const { return std::get_if<std::unordered_map<std::basic_string<C>, Value<C>>>(this); }
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;
}
};
template <typename C, int maxDepth = 25>
Value<C> Parse(const std::basic_string<C>& 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<C> 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<C>::FromCodepoint(strtol(charCode, nullptr, 16));
i += 5;
continue;
}
for (auto [original, value] : Array<char, char>{ { '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<C>::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<C>::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<C>::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<Value<C>> array;
while (true)
{
i += 1;
if (SkipWhitespace()) return {};
if (ch == ']') return i += 1, Value<C>(array);
if (!array.emplace_back(Parse<C, maxDepth>(text, i, depth + 1))) return {};
if (SkipWhitespace()) return {};
if (ch == ']') return i += 1, Value<C>(array);
if (ch != ',') return {};
}
}
if (ch == '{')
{
std::unordered_map<std::basic_string<C>, Value<C>> object;
while (true)
{
i += 1;
if (SkipWhitespace()) return {};
if (ch == '}') return i += 1, Value<C>(object);
if (ch != '"') return {};
auto key = ExtractString();
if (SkipWhitespace() || ch != ':') return {};
i += 1;
if (!(object[std::move(key)] = Parse<C, maxDepth>(text, i, depth + 1))) return {};
if (SkipWhitespace()) return {};
if (ch == '}') return i += 1, Value<C>(object);
if (ch != ',') return {};
}
}
return {};
}
template <typename C>
Value<C> Parse(const std::basic_string<C>& text)
{
int64_t start = 0;
return Parse(text, start, 0);
}
}

View File

@ -21,6 +21,7 @@ public:
Window()
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
ui.setupUi(this);
connect(ui.input, &QLineEdit::textEdited, this, &Window::setRegex);

View File

@ -17,6 +17,7 @@ public:
Window()
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
localize();
connect(&linkButton, &QPushButton::clicked, this, &Window::Link);
layout.addWidget(&linkList);

View File

@ -26,10 +26,10 @@ extern int tokenCount, tokenRestoreDelay, maxSentenceSize;
std::pair<bool, std::wstring> 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<std::wstring> translateTo = L"en", apiKey;
Synchronized<std::map<std::wstring, std::wstring>> 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));

View File

@ -27,23 +27,15 @@ constexpr bool x64 = true;
constexpr bool x64 = false;
#endif
template <typename T, typename... Xs>
struct ArrayImpl { using Type = std::tuple<T, Xs...>[]; };
template <typename T>
struct ArrayImpl<T> { using Type = T[]; };
template <typename... Ts>
using Array = typename ArrayImpl<Ts...>::Type;
template <typename T, typename... Xs> struct ArrayImpl { using Type = std::tuple<T, Xs...>[]; };
template <typename T> struct ArrayImpl<T> { using Type = T[]; };
template <typename... Ts> using Array = typename ArrayImpl<Ts...>::Type;
template <auto F>
using Functor = std::integral_constant<std::remove_reference_t<decltype(F)>, F>;
template <typename V>
struct Identity { V operator()(V v) const { return v; } };
template <auto F> using Functor = std::integral_constant<std::remove_reference_t<decltype(F)>, F>; // shouldn't need remove_reference_t but MSVC is bugged
struct PermissivePointer
{
template <typename T>
operator T*() { return (T*)p; }
template <typename T> 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 <typename T>
operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
inline static BYTE DUMMY[100];
template <typename T> operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
} DUMMY;
template <typename T>
inline auto FormatArg(T arg) { return arg; }
template <typename T> std::optional<std::remove_cv_t<T>> Copy(T* ptr) { if (ptr) return *ptr; return {}; }
template <typename C>
inline auto FormatArg(const std::basic_string<C>& arg) { return arg.c_str(); }
template <typename T> inline auto FormatArg(T arg) { return arg; }
template <typename C> inline auto FormatArg(const std::basic_string<C>& 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 <typename... Args>
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

View File

@ -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); }

View File

@ -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);

View File

@ -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