forked from Public-Mirror/Textractor
Compare commits
207 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
df5b830d17 | ||
|
f3fbe04409 | ||
|
5adeb1fab7 | ||
|
5c7b41dd84 | ||
|
92e2e85191 | ||
|
0bc23ba2c7 | ||
|
fd8a090e28 | ||
|
aa00d9e789 | ||
|
cfa11c5646 | ||
|
446f6d44e6 | ||
|
ff1a7a358c | ||
|
60a5d74eda | ||
|
4cebe9462f | ||
|
d97ebd4fbb | ||
|
233e75225c | ||
|
72ad51f039 | ||
|
39fcfaf644 | ||
|
5eb0440f01 | ||
|
c4ca84d39b | ||
|
c7b818aa75 | ||
|
2cb45ab9e3 | ||
|
72132239a5 | ||
|
5eaafbe81c | ||
|
97f2bc6304 | ||
|
f732f488e6 | ||
|
3e283975c8 | ||
|
5b35f121d2 | ||
|
b28aa3d189 | ||
|
faeda02f2b | ||
|
ef55ef0c61 | ||
|
c9240d9e68 | ||
|
6ba4f9c909 | ||
|
7c9f861926 | ||
|
e158219266 | ||
|
3a9fdfb9ae | ||
|
dad036083a | ||
|
b09783d111 | ||
|
306b9f0525 | ||
|
823987e79f | ||
|
fdc8a13b36 | ||
|
90b58da867 | ||
|
2316f59415 | ||
|
06b93e3def | ||
|
fde588d922 | ||
|
8e0e17c59b | ||
|
e6ee5ecf3d | ||
|
49133974b3 | ||
|
4b67b8a5cf | ||
|
663bb97fff | ||
|
953a537cb8 | ||
|
8ac4a072a9 | ||
|
45279d26dc | ||
|
5df90e2c01 | ||
|
0346f95707 | ||
|
1f19a9e74d | ||
|
b236a50a8f | ||
|
2f08ab1e9b | ||
|
ad3f09d8ef | ||
|
44c8e4f94c | ||
|
a17d8d5993 | ||
|
bf49f0bca6 | ||
|
15db478e62 | ||
|
46bc6ec84b | ||
|
b3b665fb65 | ||
|
17b5884149 | ||
|
742b7cacf3 | ||
|
4ff150b674 | ||
|
e83579ed7c | ||
|
febff243d3 | ||
|
1a29f5670a | ||
|
6ed3d9c951 | ||
|
492c843e5c | ||
|
574eeec79e | ||
|
6c74df864a | ||
|
ca93120442 | ||
|
eddf1bf0ee | ||
|
c9ad8880fb | ||
|
fb9fb5d54a | ||
|
085bec6d5b | ||
|
695672ce9c | ||
|
bda08d500e | ||
|
5862bf50ac | ||
|
493e80e568 | ||
|
db31e19997 | ||
|
d64a2c05b4 | ||
|
f8874bf8a0 | ||
|
b28b68d218 | ||
|
50f5b183f1 | ||
|
a566818ad4 | ||
|
ac95d873f1 | ||
|
444974ee8a | ||
|
3b948c3359 | ||
|
1eaa054b33 | ||
|
647058e9aa | ||
|
918c877e04 | ||
|
2cf146652d | ||
|
89198fd4fe | ||
|
9e0c95be98 | ||
|
7425ae770f | ||
|
e0981f89e8 | ||
|
eb1421c143 | ||
|
a1d3abb080 | ||
|
dd4b8cfbb5 | ||
|
615d372eeb | ||
|
e4cea83ae4 | ||
|
9e26c462e7 | ||
|
0df2526560 | ||
|
de3cad37f8 | ||
|
8c9faf9947 | ||
|
76fea31fb3 | ||
|
795ecce45e | ||
|
675695cde1 | ||
|
9f8e523ce3 | ||
|
d74dcdc286 | ||
|
b4aa113fac | ||
|
1e03e69d00 | ||
|
76d716956d | ||
|
1782292662 | ||
|
3c33d11d80 | ||
|
a8ae2156d9 | ||
|
f26f30dce7 | ||
|
1fcacc7bc9 | ||
|
71fe1410c2 | ||
|
aa0c0e0047 | ||
|
74121d7484 | ||
|
8543d49192 | ||
|
5537679442 | ||
|
20f35a02ed | ||
|
1bab6956a8 | ||
|
acc85f3a86 | ||
|
54a285b53b | ||
|
bf97055155 | ||
|
ad83eb290a | ||
|
c7ae06c121 | ||
|
fdc53a393d | ||
|
68b382e5fa | ||
|
9e6f638488 | ||
|
fcd735283d | ||
|
a03c939043 | ||
|
faa14f8cb9 | ||
|
e00b831e3d | ||
|
084f26a72d | ||
|
4219115a40 | ||
|
43aa1d6711 | ||
|
662ca45321 | ||
|
dcff7e550c | ||
|
c62c586a59 | ||
|
fc81b17a3c | ||
|
3a34f989e5 | ||
|
2c9ac1da3c | ||
|
95b145bece | ||
|
457aed96c2 | ||
|
4783469a0a | ||
|
0680de413a | ||
|
29d23935ad | ||
|
3aee03a244 | ||
|
268f40771d | ||
|
7eb45899c0 | ||
|
ea6ddc7a7c | ||
|
6958b22e68 | ||
|
9bb7fbff06 | ||
|
1b71b3ee86 | ||
|
1634120669 | ||
|
f8b4bc1ae3 | ||
|
01b6b6de16 | ||
|
4b46057800 | ||
|
93b8711bf8 | ||
|
8ab1e576bd | ||
|
3af4217075 | ||
|
2ab780a491 | ||
|
73d1f21bc1 | ||
|
a3ebaf0023 | ||
|
f0a1690c92 | ||
|
6b54ec0733 | ||
|
fe1cdfc947 | ||
|
f6742de3d5 | ||
|
dc1f819952 | ||
|
8e67827ae6 | ||
|
93238e13ed | ||
|
ef9d05f10e | ||
|
b4303e4d4d | ||
|
39b0882bbf | ||
|
79058c6811 | ||
|
681fc5b628 | ||
|
cc7d02d8a1 | ||
|
409ad121ae | ||
|
ae35f82199 | ||
|
51b217169e | ||
|
261da66d74 | ||
|
fe3e152553 | ||
|
341b6915c8 | ||
|
6da63208c7 | ||
|
9c006bce17 | ||
|
c3c73d2ac2 | ||
|
ae5fa5fb9f | ||
|
d86f22877c | ||
|
287776903a | ||
|
166ec1456b | ||
|
5739af5a3f | ||
|
4b0fc12794 | ||
|
3e21a6b4ba | ||
|
a2d1374ad4 | ||
|
a129394fb5 | ||
|
e20a12862c | ||
|
9711ce94c9 | ||
|
535dac480a | ||
|
d1887db2a5 |
39
.appveyor.yml
Normal file
39
.appveyor.yml
Normal file
@ -0,0 +1,39 @@
|
||||
version: '{branch}-{build}'
|
||||
configuration: RelWithDebInfo
|
||||
clone_folder: C:\Textractor
|
||||
skip_branch_with_pr: false
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
arch: x64
|
||||
platform: x64
|
||||
qtbin: msvc2017_64
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
arch: x86
|
||||
platform: Win32
|
||||
qtbin: msvc2017
|
||||
|
||||
before_build:
|
||||
- git submodule update --init
|
||||
- cd C:\
|
||||
- mkdir %arch%
|
||||
- cd %arch%
|
||||
- cmake -G "Visual Studio 15 2017" -A %platform% -DQt5_DIR="C:\Qt\5.13.2\%qtbin%\lib\cmake\Qt5" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DVERSION="" ../Textractor
|
||||
|
||||
build:
|
||||
project: C:\%arch%\Textractor.sln
|
||||
parallel: false
|
||||
verbosity: normal
|
||||
|
||||
after_build:
|
||||
- 7z a Textractor-Alpha%APPVEYOR_BUILD_NUMBER%-%arch%.zip C:\Textractor\builds\RelWithDebInfo_%arch%\*
|
||||
- appveyor PushArtifact Textractor-Alpha%APPVEYOR_BUILD_NUMBER%-%arch%.zip
|
||||
|
||||
notifications:
|
||||
- provider: Email
|
||||
to:
|
||||
- akashmozumdar@gmail.com
|
||||
on_build_success: false
|
||||
on_build_failure: true
|
||||
on_build_status_changed: true
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "texthook/minhook"]
|
||||
path = texthook/minhook
|
||||
url = https://github.com/TsudaKageyu/minhook.git
|
@ -1,6 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(Textractor)
|
||||
|
||||
if (NOT MSVC)
|
||||
message(FATAL_ERROR "Textractor can only be built with Visual Studio")
|
||||
endif()
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||
|
||||
add_compile_options(
|
||||
@ -13,29 +18,33 @@ add_compile_options(
|
||||
)
|
||||
|
||||
if(${CMAKE_SIZEOF_VOID_P} EQUAL 8)
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x64)
|
||||
link_directories(x64libs)
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x64)
|
||||
link_directories(x64libs)
|
||||
else()
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x86)
|
||||
link_directories(x86libs)
|
||||
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x86)
|
||||
link_directories(x86libs)
|
||||
endif()
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY $<1:${CMAKE_FINAL_OUTPUT_DIRECTORY}>)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY $<1:${CMAKE_FINAL_OUTPUT_DIRECTORY}>)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_FINAL_OUTPUT_DIRECTORY}>)
|
||||
|
||||
include_directories(include)
|
||||
add_library(pch text.cpp)
|
||||
target_precompile_headers(pch PUBLIC include/common.h)
|
||||
|
||||
file(GLOB ASSETS assets/*)
|
||||
file(COPY ${ASSETS} DESTINATION ${CMAKE_FINAL_OUTPUT_DIRECTORY})
|
||||
|
||||
include(QtUtils)
|
||||
msvc_registry_search()
|
||||
find_qt5(Core Widgets WinExtras WebSockets)
|
||||
|
||||
add_library(text text.cpp)
|
||||
target_compile_definitions(text PRIVATE ${TEXT_LANGUAGE})
|
||||
link_libraries(text)
|
||||
|
||||
add_subdirectory(GUI)
|
||||
add_subdirectory(host)
|
||||
add_subdirectory(texthook)
|
||||
add_subdirectory(GUI)
|
||||
add_subdirectory(extensions)
|
||||
add_subdirectory(test)
|
||||
if (DEFINED VERSION)
|
||||
add_subdirectory(GUI/host)
|
||||
endif()
|
||||
#add_subdirectory(GUI/host)
|
||||
|
@ -1,29 +1,32 @@
|
||||
{
|
||||
// See https://go.microsoft.com//fwlink//?linkid=834763 for more information about this file.
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x86-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"cmakeCommandArgs": "-DQT_ROOT=/Qt",
|
||||
"inheritEnvironments": [ "msvc_x86" ]
|
||||
},
|
||||
{
|
||||
"name": "x86-RelWithDebInfo",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "RelWithDebInfo",
|
||||
"cmakeCommandArgs": "-DQT_ROOT=/Qt",
|
||||
"inheritEnvironments": [ "msvc_x86" ]
|
||||
},
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"cmakeCommandArgs": "-DQT_ROOT=/Qt",
|
||||
"inheritEnvironments": [ "msvc_x64" ]
|
||||
},
|
||||
{
|
||||
"name": "x64-RelWithDebInfo",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "RelWithDebInfo",
|
||||
"cmakeCommandArgs": "-DQT_ROOT=/Qt",
|
||||
"inheritEnvironments": [ "msvc_x64" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
27
CREDITS.md
27
CREDITS.md
@ -1,27 +0,0 @@
|
||||
# Developers
|
||||
|
||||
If you're on this list and want your link changed let me know.
|
||||
- Textractor mainly made by [Me](https://github.com/Artikash) with the help of
|
||||
- [DoumanAsh](https://github.com/DoumanAsh)
|
||||
- [Niakr1s](https://github.com/Niakr1s)
|
||||
- [tinyAdapter](https://github.com/tinyAdapter)
|
||||
- Spanish translation by [scese250](https://github.com/scese250)
|
||||
- Turkish translation by niisokusu
|
||||
- Simplified Chinese translation by [tinyAdapter](https://github.com/tinyAdapter)
|
||||
- Russian translation by [TokcDK](https://github.com/TokcDK)
|
||||
- Indonesian translation by [Hawxone](https://github.com/Hawxone)
|
||||
- Portuguese translation by [TsumiHokiro](https://github.com/TsumiHokiro)
|
||||
- Thai translation by [azmadoppler](https://github.com/azmadoppler)
|
||||
- Korean translation by O SK
|
||||
- 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)
|
||||
- ITH updated by [Andys](https://github.com/AndyScull)
|
||||
- ITH originally made by [kaosu](http://www.hongfire.com/forum/member/562651-kaosu)
|
||||
- Locale Emulator library made by [xupefei](https://github.com/xupefei)
|
||||
- MinHook library made by [TsudaKageyu](https://github.com/TsudaKageyu)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
- Everybody adding issues!
|
@ -1,22 +1,20 @@
|
||||
include(QtUtils)
|
||||
msvc_registry_search()
|
||||
find_qt5(Core Widgets)
|
||||
|
||||
add_executable(Textractor WIN32
|
||||
main.cpp
|
||||
exception.cpp
|
||||
mainwindow.cpp
|
||||
extenwindow.cpp
|
||||
host/exception.cpp
|
||||
host/host.cpp
|
||||
host/textthread.cpp
|
||||
host/hookcode.cpp
|
||||
extenwindow.cpp
|
||||
attachprocessdialog.cpp
|
||||
Textractor.rc
|
||||
Textractor.ico
|
||||
)
|
||||
target_link_libraries(${PROJECT_NAME} Qt5::Widgets shell32 winhttp)
|
||||
target_precompile_headers(Textractor REUSE_FROM pch)
|
||||
target_link_libraries(Textractor host Qt5::Widgets Qt5::WinExtras 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()
|
||||
|
39
GUI/attachprocessdialog.cpp
Normal file
39
GUI/attachprocessdialog.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#include "attachprocessdialog.h"
|
||||
#include <QtWinExtras/QtWin>
|
||||
|
||||
extern const char* SELECT_PROCESS;
|
||||
extern const char* ATTACH_INFO;
|
||||
|
||||
AttachProcessDialog::AttachProcessDialog(QWidget* parent, std::vector<std::pair<QString, HICON>> processIcons) :
|
||||
QDialog(parent, Qt::WindowCloseButtonHint),
|
||||
model(this)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
setWindowTitle(SELECT_PROCESS);
|
||||
ui.label->setText(ATTACH_INFO);
|
||||
ui.processList->setModel(&model);
|
||||
|
||||
QPixmap transparent(100, 100);
|
||||
transparent.fill(QColor::fromRgba(0));
|
||||
for (const auto& [process, icon] : processIcons)
|
||||
{
|
||||
auto item = new QStandardItem(icon ? QIcon(QtWin::fromHICON(icon)) : transparent, process);
|
||||
item->setEditable(false);
|
||||
model.appendRow(item);
|
||||
}
|
||||
|
||||
connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(ui.processList, &QListView::clicked, [this](QModelIndex index) { ui.processEdit->setText(model.item(index.row())->text()); });
|
||||
connect(ui.processList, &QListView::doubleClicked, this, &QDialog::accept);
|
||||
connect(ui.processEdit, &QLineEdit::textEdited, [this](QString process)
|
||||
{
|
||||
for (int i = 0; i < model.rowCount(); ++i) ui.processList->setRowHidden(i, !model.item(i)->text().contains(process, Qt::CaseInsensitive));
|
||||
});
|
||||
connect(ui.processEdit, &QLineEdit::returnPressed, this, &QDialog::accept);
|
||||
}
|
||||
|
||||
QString AttachProcessDialog::SelectedProcess()
|
||||
{
|
||||
return ui.processEdit->text();
|
||||
}
|
17
GUI/attachprocessdialog.h
Normal file
17
GUI/attachprocessdialog.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "qtcommon.h"
|
||||
#include "ui_attachprocessdialog.h"
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class AttachProcessDialog : public QDialog
|
||||
{
|
||||
public:
|
||||
explicit AttachProcessDialog(QWidget* parent, std::vector<std::pair<QString, HICON>> processIcons);
|
||||
QString SelectedProcess();
|
||||
|
||||
private:
|
||||
Ui::AttachProcessDialog ui;
|
||||
QStandardItemModel model;
|
||||
QString selectedProcess;
|
||||
};
|
37
GUI/attachprocessdialog.ui
Normal file
37
GUI/attachprocessdialog.ui
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AttachProcessDialog</class>
|
||||
<widget class="QWidget" name="AttachProcessDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="processEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="processList"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -1,4 +1,3 @@
|
||||
#include "common.h"
|
||||
#include "module.h"
|
||||
#include <sstream>
|
||||
|
||||
@ -6,7 +5,7 @@ namespace
|
||||
{
|
||||
char* GetCppExceptionInfo(EXCEPTION_POINTERS* exception)
|
||||
{
|
||||
// See https://blogs.msdn.microsoft.com/oldnewthing/20100730-00/?p=13273
|
||||
// https://blogs.msdn.microsoft.com/oldnewthing/20100730-00/?p=13273
|
||||
// Not very reliable so use __try
|
||||
__try { return ((char****)exception->ExceptionRecord->ExceptionInformation[2])[3][1][1] + 8; }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) { return "Could not find"; }
|
||||
@ -22,12 +21,11 @@ namespace
|
||||
|
||||
__declspec(noreturn) void Terminate()
|
||||
{
|
||||
CreateThread(nullptr, 0, [](void* lastError) -> DWORD
|
||||
WaitForSingleObject(CreateThread(nullptr, 0, [](void* lastError) -> DWORD
|
||||
{
|
||||
MessageBoxW(NULL, (wchar_t*)lastError, L"Textractor ERROR", MB_ICONERROR); // might fail to display if called in main thread and exception was in main event loop
|
||||
abort();
|
||||
}, lastError.data(), 0, nullptr);
|
||||
Sleep(MAXDWORD);
|
||||
}, lastError.data(), 0, nullptr), INFINITE);
|
||||
}
|
||||
|
||||
LONG WINAPI ExceptionLogger(EXCEPTION_POINTERS* exception)
|
@ -1,6 +1,5 @@
|
||||
#include "extenwindow.h"
|
||||
#include "ui_extenwindow.h"
|
||||
#include <concrt.h>
|
||||
#include <QMenu>
|
||||
#include <QFileDialog>
|
||||
#include <QDragEnterEvent>
|
||||
@ -10,6 +9,7 @@
|
||||
|
||||
extern const char* EXTENSIONS;
|
||||
extern const char* ADD_EXTENSION;
|
||||
extern const char* REMOVE_EXTENSION;
|
||||
extern const char* INVALID_EXTENSION;
|
||||
extern const char* CONFIRM_EXTENSION_OVERWRITE;
|
||||
extern const char* EXTENSION_WRITE_ERROR;
|
||||
@ -18,7 +18,7 @@ extern const char* EXTEN_WINDOW_INSTRUCTIONS;
|
||||
namespace
|
||||
{
|
||||
constexpr auto EXTEN_SAVE_FILE = u8"SavedExtensions.txt";
|
||||
constexpr auto DEFAULT_EXTENSIONS = u8"Remove Repeated Characters>Remove Repeated Phrases>Regex Filter>Copy to Clipboard>Bing Translate>Extra Window>Extra Newlines";
|
||||
constexpr auto DEFAULT_EXTENSIONS = u8"Remove Repeated Characters>Regex Filter>Copy to Clipboard>Google Translate>Extra Window>Extra Newlines";
|
||||
|
||||
struct Extension
|
||||
{
|
||||
@ -33,14 +33,17 @@ namespace
|
||||
|
||||
bool Load(QString extenName)
|
||||
{
|
||||
// Extension is dll and exports "OnNewSentence"
|
||||
if (QTextFile(extenName + ".dll", QIODevice::ReadOnly).readAll().contains("OnNewSentence"))
|
||||
if (extenName.endsWith(".dll")) extenName.chop(4);
|
||||
if (extenName.endsWith(".xdll")) extenName.chop(5);
|
||||
if (!QFile::exists(extenName + ".xdll")) QFile::copy(extenName + ".dll", extenName + ".xdll");
|
||||
// Extension must export "OnNewSentence"
|
||||
if (QTextFile(extenName + ".xdll", QIODevice::ReadOnly).readAll().contains("OnNewSentence"))
|
||||
{
|
||||
if (HMODULE module = LoadLibraryW(S(extenName + ".dll").c_str()))
|
||||
if (HMODULE module = LoadLibraryW(S(extenName + ".xdll").c_str()))
|
||||
{
|
||||
if (auto callback = (decltype(Extension::callback))GetProcAddress(module, "OnNewSentence"))
|
||||
{
|
||||
std::scoped_lock writeLock(extenMutex);
|
||||
std::scoped_lock lock(extenMutex);
|
||||
extensions.push_back({ S(extenName), callback });
|
||||
return true;
|
||||
}
|
||||
@ -52,14 +55,14 @@ namespace
|
||||
|
||||
void Unload(int index)
|
||||
{
|
||||
std::scoped_lock writeLock(extenMutex);
|
||||
FreeLibrary(GetModuleHandleW((extensions.at(index).name + L".dll").c_str()));
|
||||
std::scoped_lock lock(extenMutex);
|
||||
FreeLibrary(GetModuleHandleW((extensions.at(index).name + L".xdll").c_str()));
|
||||
extensions.erase(extensions.begin() + index);
|
||||
}
|
||||
|
||||
void Reorder(QStringList extenNames)
|
||||
{
|
||||
std::scoped_lock writeLock(extenMutex);
|
||||
std::scoped_lock lock(extenMutex);
|
||||
std::vector<Extension> extensions;
|
||||
for (auto extenName : extenNames)
|
||||
extensions.push_back(*std::find_if(::extensions.begin(), ::extensions.end(), [&](Extension extension) { return extension.name == S(extenName); }));
|
||||
@ -80,23 +83,33 @@ namespace
|
||||
|
||||
void Add(QFileInfo extenFile)
|
||||
{
|
||||
if (extenFile.suffix() == "dll")
|
||||
if (extenFile.suffix() == "dll" || extenFile.suffix() == "xdll")
|
||||
{
|
||||
if (extenFile.absolutePath() != QDir::currentPath())
|
||||
{
|
||||
if (QFile::exists(extenFile.fileName()) && QMessageBox::question(This, EXTENSIONS, CONFIRM_EXTENSION_OVERWRITE) == QMessageBox::Yes) QFile::remove(extenFile.fileName());
|
||||
if (!QFile::copy(extenFile.absoluteFilePath(), extenFile.fileName())) QMessageBox::warning(This, EXTENSIONS, EXTENSION_WRITE_ERROR);
|
||||
}
|
||||
if (Load(extenFile.completeBaseName())) return Sync();
|
||||
if (Load(extenFile.fileName())) return Sync();
|
||||
}
|
||||
QMessageBox::information(This, EXTENSIONS, QString(INVALID_EXTENSION).arg(extenFile.fileName()));
|
||||
}
|
||||
|
||||
void OpenMenu(QPoint point)
|
||||
void Delete()
|
||||
{
|
||||
QAction addExtension(ADD_EXTENSION);
|
||||
if (QMenu::exec({ &addExtension }, ui.extenList->mapToGlobal(point), nullptr, This))
|
||||
if (QString extenFile = QFileDialog::getOpenFileName(This, ADD_EXTENSION, ".", EXTENSIONS + QString(" (*.dll)")); !extenFile.isEmpty()) Add(extenFile);
|
||||
if (ui.extenList->currentItem())
|
||||
{
|
||||
Unload(ui.extenList->currentIndex().row());
|
||||
Sync();
|
||||
}
|
||||
}
|
||||
|
||||
void ContextMenu(QPoint point)
|
||||
{
|
||||
QAction addExtension(ADD_EXTENSION), removeExtension(REMOVE_EXTENSION);
|
||||
if (auto action = QMenu::exec({ &addExtension, &removeExtension }, ui.extenList->mapToGlobal(point), nullptr, This))
|
||||
if (action == &removeExtension) Delete();
|
||||
else if (QString extenFile = QFileDialog::getOpenFileName(This, ADD_EXTENSION, ".", EXTENSIONS + QString(" (*.xdll);;Libraries (*.dll)")); !extenFile.isEmpty()) Add(extenFile);
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +119,7 @@ bool DispatchSentenceToExtensions(std::wstring& sentence, const InfoForExtension
|
||||
wcscpy_s(sentenceBuffer, sentence.size() + 1, sentence.c_str());
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(extenMutex);
|
||||
for (const auto& extension : extensions)
|
||||
if (*(sentenceBuffer = extension.callback(sentenceBuffer, sentenceInfo)) == L'\0') break;
|
||||
if (!*(sentenceBuffer = extension.callback(sentenceBuffer, sentenceInfo))) break;
|
||||
sentence = sentenceBuffer;
|
||||
HeapFree(GetProcessHeap(), 0, sentenceBuffer);
|
||||
return !sentence.empty();
|
||||
@ -114,20 +127,19 @@ bool DispatchSentenceToExtensions(std::wstring& sentence, const InfoForExtension
|
||||
|
||||
void CleanupExtensions()
|
||||
{
|
||||
std::scoped_lock writeLock(extenMutex);
|
||||
for (auto extension : extensions) FreeLibrary(GetModuleHandleW((extension.name + L".dll").c_str()));
|
||||
std::scoped_lock lock(extenMutex);
|
||||
for (auto extension : extensions) FreeLibrary(GetModuleHandleW((extension.name + L".xdll").c_str()));
|
||||
extensions.clear();
|
||||
}
|
||||
|
||||
ExtenWindow::ExtenWindow(QWidget* parent) :
|
||||
QMainWindow(parent, Qt::WindowCloseButtonHint)
|
||||
ExtenWindow::ExtenWindow(QWidget* parent) : QMainWindow(parent, Qt::WindowCloseButtonHint)
|
||||
{
|
||||
This = this;
|
||||
ui.setupUi(this);
|
||||
ui.vboxLayout->addWidget(new QLabel(EXTEN_WINDOW_INSTRUCTIONS, this));
|
||||
setWindowTitle(EXTENSIONS);
|
||||
|
||||
connect(ui.extenList, &QListWidget::customContextMenuRequested, OpenMenu);
|
||||
connect(ui.extenList, &QListWidget::customContextMenuRequested, ContextMenu);
|
||||
ui.extenList->installEventFilter(this);
|
||||
|
||||
if (!QFile::exists(EXTEN_SAVE_FILE)) QTextFile(EXTEN_SAVE_FILE, QIODevice::WriteOnly).write(DEFAULT_EXTENSIONS);
|
||||
@ -135,11 +147,9 @@ 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
|
||||
// https://stackoverflow.com/questions/1224432/how-do-i-respond-to-an-internal-drag-and-drop-operation-using-a-qlistwidget/1528215
|
||||
if (event->type() == QEvent::ChildRemoved)
|
||||
{
|
||||
QStringList extenNames;
|
||||
@ -152,11 +162,7 @@ bool ExtenWindow::eventFilter(QObject* target, QEvent* event)
|
||||
|
||||
void ExtenWindow::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Delete && ui.extenList->currentItem())
|
||||
{
|
||||
Unload(ui.extenList->currentIndex().row());
|
||||
Sync();
|
||||
}
|
||||
if (event->key() == Qt::Key_Delete) Delete();
|
||||
}
|
||||
|
||||
void ExtenWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||
|
@ -2,11 +2,6 @@
|
||||
|
||||
#include "qtcommon.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class ExtenWindow;
|
||||
}
|
||||
|
||||
struct InfoForExtension
|
||||
{
|
||||
const char* name;
|
||||
@ -20,7 +15,6 @@ class ExtenWindow : public QMainWindow
|
||||
{
|
||||
public:
|
||||
explicit ExtenWindow(QWidget* parent = nullptr);
|
||||
~ExtenWindow();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject* target, QEvent* event) override;
|
||||
|
@ -1,8 +0,0 @@
|
||||
# The CLI isn't used by Textractor itself, but is here for other people that want to build projects on top of Textractor
|
||||
add_executable(TextractorCLI
|
||||
cli.cpp
|
||||
exception.cpp
|
||||
host.cpp
|
||||
textthread.cpp
|
||||
hookcode.cpp
|
||||
)
|
@ -1,7 +1,6 @@
|
||||
#include "mainwindow.h"
|
||||
#include "module.h"
|
||||
#include <winhttp.h>
|
||||
#include <QApplication>
|
||||
|
||||
extern const wchar_t* UPDATE_AVAILABLE;
|
||||
|
||||
|
@ -3,8 +3,9 @@
|
||||
#include "defs.h"
|
||||
#include "module.h"
|
||||
#include "extenwindow.h"
|
||||
#include "host/host.h"
|
||||
#include "host/hookcode.h"
|
||||
#include "../host/host.h"
|
||||
#include "../host/hookcode.h"
|
||||
#include "attachprocessdialog.h"
|
||||
#include <shellapi.h>
|
||||
#include <process.h>
|
||||
#include <QRegularExpression>
|
||||
@ -14,10 +15,11 @@
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QHash>
|
||||
|
||||
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;
|
||||
@ -28,12 +30,13 @@ extern const char* SETTINGS;
|
||||
extern const char* EXTENSIONS;
|
||||
extern const char* FONT;
|
||||
extern const char* SELECT_PROCESS;
|
||||
extern const char* ATTACH_INFO;
|
||||
extern const char* SELECT_PROCESS_INFO;
|
||||
extern const char* FROM_COMPUTER;
|
||||
extern const char* PROCESSES;
|
||||
extern const char* CODE_INFODUMP;
|
||||
extern const char* FAILED_TO_CREATE_CONFIG_FILE;
|
||||
extern const char* HOOK_SEARCH_UNSTABLE_WARNING;
|
||||
extern const char* HOOK_SEARCH_STARTING_VIEW_CONSOLE;
|
||||
extern const char* SEARCH_CJK;
|
||||
extern const char* SEARCH_PATTERN;
|
||||
extern const char* SEARCH_DURATION;
|
||||
@ -85,6 +88,8 @@ namespace
|
||||
TextThread* current = nullptr;
|
||||
MainWindow* This = nullptr;
|
||||
|
||||
void FindHooks();
|
||||
|
||||
QString TextThreadString(TextThread& thread)
|
||||
{
|
||||
return QString("%1:%2:%3:%4:%5: %6").arg(
|
||||
@ -98,16 +103,20 @@ namespace
|
||||
|
||||
ThreadParam ParseTextThreadString(QString ttString)
|
||||
{
|
||||
QStringList threadParam = ttString.split(":");
|
||||
auto threadParam = ttString.splitRef(":");
|
||||
return { threadParam[1].toUInt(nullptr, 16), threadParam[2].toULongLong(nullptr, 16), threadParam[3].toULongLong(nullptr, 16), threadParam[4].toULongLong(nullptr, 16) };
|
||||
}
|
||||
|
||||
std::array<InfoForExtension, 10> GetSentenceInfo(TextThread& thread)
|
||||
std::array<InfoForExtension, 20> GetSentenceInfo(TextThread& thread)
|
||||
{
|
||||
void (*AddText)(int64_t, const wchar_t*) = [](int64_t number, const wchar_t* text)
|
||||
{
|
||||
QMetaObject::invokeMethod(This, [number, text = std::wstring(text)] { if (TextThread* thread = Host::GetThread(number)) thread->Push(text.c_str()); });
|
||||
};
|
||||
void (*AddSentence)(int64_t, const wchar_t*) = [](int64_t number, const wchar_t* sentence)
|
||||
{
|
||||
// pointer from Host::GetThread may not stay valid unless on main thread
|
||||
QMetaObject::invokeMethod(This, [number, sentence = std::wstring(sentence)]{ if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(sentence); });
|
||||
QMetaObject::invokeMethod(This, [number, sentence = std::wstring(sentence)] { if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(sentence); });
|
||||
};
|
||||
DWORD (*GetSelectedProcessId)() = [] { return selectedProcessId.load(); };
|
||||
|
||||
@ -119,12 +128,31 @@ namespace
|
||||
{ "hook address", (int64_t)thread.tp.addr },
|
||||
{ "text handle", thread.handle },
|
||||
{ "text name", (int64_t)thread.name.c_str() },
|
||||
{ "add sentence", (int64_t)AddSentence },
|
||||
{ "add text", (int64_t)AddText },
|
||||
{ "get selected process id", (int64_t)GetSelectedProcessId },
|
||||
{ "void (*AddSentence)(int64_t number, const wchar_t* sentence)", (int64_t)AddSentence },
|
||||
{ "void (*AddText)(int64_t number, const wchar_t* text)", (int64_t)AddText },
|
||||
{ "DWORD (*GetSelectedProcessId)()", (int64_t)GetSelectedProcessId },
|
||||
{ nullptr, 0 } // nullptr marks end of info array
|
||||
} };
|
||||
}
|
||||
|
||||
void AttachSavedProcesses()
|
||||
{
|
||||
std::unordered_set<std::wstring> attachTargets;
|
||||
if (autoAttach)
|
||||
for (auto process : QString(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process));
|
||||
if (autoAttachSavedOnly)
|
||||
for (auto process : QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process.split(" , ")[0]));
|
||||
|
||||
if (!attachTargets.empty())
|
||||
for (auto [processId, processName] : GetAllProcesses())
|
||||
if (processName && attachTargets.count(processName.value()) > 0 && alreadyAttached.count(processId) == 0) Host::InjectProcess(processId);
|
||||
}
|
||||
|
||||
std::optional<std::wstring> UserSelectedProcess()
|
||||
{
|
||||
QStringList savedProcesses = QString::fromUtf8(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts);
|
||||
@ -132,23 +160,45 @@ namespace
|
||||
savedProcesses.removeDuplicates();
|
||||
savedProcesses.insert(1, FROM_COMPUTER);
|
||||
QString process = QInputDialog::getItem(This, SELECT_PROCESS, SELECT_PROCESS_INFO, savedProcesses, 0, true, &ok, Qt::WindowCloseButtonHint);
|
||||
if (process == FROM_COMPUTER) process = QDir::toNativeSeparators(QFileDialog::getOpenFileName(This, SELECT_PROCESS, "C:\\", PROCESSES));
|
||||
if (process == FROM_COMPUTER) process = QDir::toNativeSeparators(QFileDialog::getOpenFileName(This, SELECT_PROCESS, "/", PROCESSES));
|
||||
if (ok && process.contains('\\')) return S(process);
|
||||
return {};
|
||||
}
|
||||
|
||||
void ViewThread(int index)
|
||||
{
|
||||
ui.ttCombo->setCurrentIndex(index);
|
||||
ui.textOutput->setPlainText(sanitize(S((current = &Host::GetThread(ParseTextThreadString(ui.ttCombo->itemText(index))))->storage->c_str())));
|
||||
ui.textOutput->moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
void AttachProcess()
|
||||
{
|
||||
QMultiHash<QString, DWORD> allProcesses;
|
||||
QMultiHash<QString, DWORD> processesMap;
|
||||
std::vector<std::pair<QString, HICON>> processIcons;
|
||||
for (auto [processId, processName] : GetAllProcesses())
|
||||
if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::wstring::npos))
|
||||
allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId);
|
||||
{
|
||||
if (processName && (showSystemProcesses || processName->find(L":\\Windows\\") == std::string::npos))
|
||||
{
|
||||
QString fileName = QFileInfo(S(processName.value())).fileName();
|
||||
if (!processesMap.contains(fileName))
|
||||
{
|
||||
HICON bigIcon, smallIcon;
|
||||
ExtractIconExW(processName->c_str(), 0, &bigIcon, &smallIcon, 1);
|
||||
processIcons.push_back({ fileName, bigIcon ? bigIcon : smallIcon });
|
||||
}
|
||||
processesMap.insert(fileName, processId);
|
||||
}
|
||||
}
|
||||
std::sort(processIcons.begin(), processIcons.end(), [](auto one, auto two) { return QString::compare(one.first, two.first, Qt::CaseInsensitive) < 0; });
|
||||
|
||||
QStringList processList(allProcesses.uniqueKeys());
|
||||
processList.sort(Qt::CaseInsensitive);
|
||||
if (QString process = QInputDialog::getItem(This, SELECT_PROCESS, ATTACH_INFO, processList, 0, true, &ok, Qt::WindowCloseButtonHint); ok)
|
||||
if (process.toInt(nullptr, 0)) Host::InjectProcess(process.toInt(nullptr, 0));
|
||||
else for (auto processId : allProcesses.values(process)) Host::InjectProcess(processId);
|
||||
AttachProcessDialog attachProcessDialog(This, processIcons);
|
||||
if (attachProcessDialog.exec())
|
||||
{
|
||||
QString process = attachProcessDialog.SelectedProcess();
|
||||
if (int processId = process.toInt(nullptr, 0)) Host::InjectProcess(processId);
|
||||
else for (int processId : processesMap.values(process)) Host::InjectProcess(processId);
|
||||
}
|
||||
}
|
||||
|
||||
void LaunchProcess()
|
||||
@ -159,12 +209,12 @@ 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"))
|
||||
{
|
||||
// see https://github.com/xupefei/Locale-Emulator/blob/aa99dec3b25708e676c90acf5fed9beaac319160/LEProc/LoaderWrapper.cs#L252
|
||||
// https://github.com/xupefei/Locale-Emulator/blob/aa99dec3b25708e676c90acf5fed9beaac319160/LEProc/LoaderWrapper.cs#L252
|
||||
struct
|
||||
{
|
||||
ULONG AnsiCodePage = SHIFT_JIS;
|
||||
@ -192,13 +242,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::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");
|
||||
_wspawnlp(_P_DETACH, L"notepad", L"notepad", configFile.c_str(), NULL);
|
||||
if (std::filesystem::exists(configFile)) _wspawnlp(_P_DETACH, L"notepad", L"notepad", configFile.c_str(), NULL);
|
||||
else QMessageBox::critical(This, CONFIG, QString(FAILED_TO_CREATE_CONFIG_FILE).arg(S(configFile)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,14 +273,12 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void FindHooks();
|
||||
|
||||
void AddHook(QString hook)
|
||||
{
|
||||
if (QString hookCode = QInputDialog::getText(This, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, hook, &ok, Qt::WindowCloseButtonHint); ok)
|
||||
if (hookCode.startsWith("S") || hookCode.startsWith("/S")) FindHooks();
|
||||
else if (auto hp = HookCode::Parse(S(hookCode))) try { Host::InsertHook(selectedProcessId, hp.value()); } catch (std::out_of_range) {}
|
||||
else Host::AddConsoleOutput(INVALID_CODE);
|
||||
if (hookCode.startsWith("S") || hookCode.startsWith("/S")) FindHooks(); // backwards compatibility for old hook search UX
|
||||
else if (auto hp = HookCode::Parse(S(hookCode))) try { Host::InsertHook(selectedProcessId, hp.value()); } catch (std::out_of_range) {}
|
||||
else Host::AddConsoleOutput(INVALID_CODE);
|
||||
}
|
||||
|
||||
void AddHook()
|
||||
@ -251,8 +300,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
|
||||
@ -297,8 +345,8 @@ namespace
|
||||
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QCheckBox cjkCheckBox(&dialog);
|
||||
layout.addRow(SEARCH_CJK, &cjkCheckBox);
|
||||
QCheckBox asianCheck(&dialog);
|
||||
layout.addRow(SEARCH_CJK, &asianCheck);
|
||||
QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help | QDialogButtonBox::Retry, &dialog);
|
||||
layout.addRow(&confirm);
|
||||
confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH);
|
||||
@ -317,30 +365,33 @@ namespace
|
||||
{
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QLineEdit textInput(&dialog);
|
||||
layout.addRow(TEXT, &textInput);
|
||||
QSpinBox codepageInput(&dialog);
|
||||
codepageInput.setMaximum(INT_MAX);
|
||||
codepageInput.setValue(sp.codepage);
|
||||
layout.addRow(CODEPAGE, &codepageInput);
|
||||
QLineEdit textEdit(&dialog);
|
||||
layout.addRow(TEXT, &textEdit);
|
||||
QSpinBox codepageSpin(&dialog);
|
||||
codepageSpin.setMaximum(INT_MAX);
|
||||
codepageSpin.setValue(sp.codepage);
|
||||
layout.addRow(CODEPAGE, &codepageSpin);
|
||||
QDialogButtonBox confirm(QDialogButtonBox::Ok);
|
||||
QObject::connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
layout.addRow(&confirm);
|
||||
if (!dialog.exec()) return;
|
||||
wcsncpy_s(sp.text, S(textInput.text()).c_str(), PATTERN_SIZE - 1);
|
||||
try { Host::FindHooks(selectedProcessId, sp); }
|
||||
catch (std::out_of_range) {}
|
||||
wcsncpy_s(sp.text, S(textEdit.text()).c_str(), PATTERN_SIZE - 1);
|
||||
try
|
||||
{
|
||||
Host::FindHooks(selectedProcessId, sp);
|
||||
ViewThread(0);
|
||||
} catch (std::out_of_range) {}
|
||||
return;
|
||||
}
|
||||
|
||||
filter.setPattern(cjkCheckBox.isChecked() ? "[\\x{3000}-\\x{a000}]{4,}" : "[\\x{0020}-\\x{1000}]{4,}");
|
||||
filter.setPattern(asianCheck.isChecked() ? "[\\x{3000}-\\x{a000}]{4,}" : "[\\x{0020}-\\x{1000}]{4,}");
|
||||
if (customSettings)
|
||||
{
|
||||
QDialog dialog(This, Qt::WindowCloseButtonHint);
|
||||
QFormLayout layout(&dialog);
|
||||
QLineEdit patternInput(x64 ? "CC CC 48 89" : "55 8B EC", &dialog);
|
||||
assert(QByteArray::fromHex(patternInput.text().toUtf8()) == QByteArray((const char*)sp.pattern, sp.length));
|
||||
layout.addRow(SEARCH_PATTERN, &patternInput);
|
||||
QLineEdit patternEdit(x64 ? "CC CC 48 89" : "55 8B EC", &dialog);
|
||||
assert(QByteArray::fromHex(patternEdit.text().toUtf8()) == QByteArray((const char*)sp.pattern, sp.length));
|
||||
layout.addRow(SEARCH_PATTERN, &patternEdit);
|
||||
for (auto [value, label] : Array<int&, const char*>{
|
||||
{ sp.searchTime, SEARCH_DURATION },
|
||||
{ sp.offset, PATTERN_OFFSET },
|
||||
@ -354,36 +405,36 @@ namespace
|
||||
layout.addRow(label, spinBox);
|
||||
QObject::connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [&value](int newValue) { value = newValue; });
|
||||
}
|
||||
QLineEdit boundInput(QFileInfo(S(GetModuleFilename(selectedProcessId).value_or(L""))).fileName(), &dialog);
|
||||
layout.addRow(SEARCH_MODULE, &boundInput);
|
||||
QLineEdit boundEdit(QFileInfo(S(GetModuleFilename(selectedProcessId).value_or(L""))).fileName(), &dialog);
|
||||
layout.addRow(SEARCH_MODULE, &boundEdit);
|
||||
for (auto [value, label] : Array<uintptr_t&, const char*>{
|
||||
{ sp.minAddress, MIN_ADDRESS },
|
||||
{ sp.maxAddress, MAX_ADDRESS },
|
||||
{ sp.padding, STRING_OFFSET },
|
||||
})
|
||||
{
|
||||
auto input = new QLineEdit(QString::number(value, 16), &dialog);
|
||||
layout.addRow(label, input);
|
||||
QObject::connect(input, &QLineEdit::textEdited, [&value](QString text) { if (uintptr_t newValue = text.toULongLong(&ok, 16); ok) value = newValue; });
|
||||
auto edit = new QLineEdit(QString::number(value, 16), &dialog);
|
||||
layout.addRow(label, edit);
|
||||
QObject::connect(edit, &QLineEdit::textEdited, [&value](QString text) { if (uintptr_t newValue = text.toULongLong(&ok, 16); ok) value = newValue; });
|
||||
}
|
||||
QLineEdit filterInput(filter.pattern(), &dialog);
|
||||
layout.addRow(HOOK_SEARCH_FILTER, &filterInput);
|
||||
QLineEdit filterEdit(filter.pattern(), &dialog);
|
||||
layout.addRow(HOOK_SEARCH_FILTER, &filterEdit);
|
||||
QPushButton startButton(START_HOOK_SEARCH, &dialog);
|
||||
layout.addWidget(&startButton);
|
||||
QObject::connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept);
|
||||
if (!dialog.exec()) return;
|
||||
if (patternInput.text().contains('.'))
|
||||
if (patternEdit.text().contains('.'))
|
||||
{
|
||||
wcsncpy_s(sp.exportModule, S(patternInput.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
wcsncpy_s(sp.exportModule, S(patternEdit.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
sp.length = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8());
|
||||
QByteArray pattern = QByteArray::fromHex(patternEdit.text().replace("??", QString::number(XX, 16)).toUtf8());
|
||||
memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), PATTERN_SIZE));
|
||||
}
|
||||
wcsncpy_s(sp.boundaryModule, S(boundInput.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
filter.setPattern(filterInput.text());
|
||||
wcsncpy_s(sp.boundaryModule, S(boundEdit.text()).c_str(), MAX_MODULE_SIZE - 1);
|
||||
filter.setPattern(filterEdit.text());
|
||||
if (!filter.isValid()) filter.setPattern(".");
|
||||
}
|
||||
else
|
||||
@ -399,10 +450,10 @@ namespace
|
||||
[hooks, filter](HookParam hp, std::wstring text) { if (filter.match(S(text)).hasMatch()) *hooks << sanitize(S(HookCode::Generate(hp) + L" => " + text)); });
|
||||
}
|
||||
catch (std::out_of_range) { return; }
|
||||
ViewThread(0);
|
||||
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, [&]
|
||||
@ -430,12 +481,13 @@ namespace
|
||||
}
|
||||
hooks->clear();
|
||||
}).detach();
|
||||
QMessageBox::information(This, SEARCH_FOR_HOOKS, HOOK_SEARCH_STARTING_VIEW_CONSOLE);
|
||||
}
|
||||
|
||||
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*>{
|
||||
@ -463,12 +515,12 @@ namespace
|
||||
layout.addRow(label, spinBox);
|
||||
QObject::connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); });
|
||||
}
|
||||
QComboBox localeComboBox(&dialog);
|
||||
QComboBox localeCombo(&dialog);
|
||||
assert(PROMPT == 0 && ALWAYS == 1 && NEVER == 2);
|
||||
localeComboBox.addItems({ { "Prompt", "Always", "Never" } });
|
||||
localeComboBox.setCurrentIndex(settings.value(CONFIG_JP_LOCALE, PROMPT).toInt());
|
||||
layout.addRow(CONFIG_JP_LOCALE, &localeComboBox);
|
||||
QObject::connect(&localeComboBox, qOverload<int>(&QComboBox::activated), [&settings](int i) { settings.setValue(CONFIG_JP_LOCALE, i); });
|
||||
localeCombo.addItems({ { "Prompt", "Always", "Never" } });
|
||||
localeCombo.setCurrentIndex(settings.value(CONFIG_JP_LOCALE, PROMPT).toInt());
|
||||
layout.addRow(CONFIG_JP_LOCALE, &localeCombo);
|
||||
QObject::connect(&localeCombo, qOverload<int>(&QComboBox::activated), [&settings](int i) { settings.setValue(CONFIG_JP_LOCALE, i); });
|
||||
layout.addWidget(&saveButton);
|
||||
QObject::connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept);
|
||||
dialog.setWindowTitle(SETTINGS);
|
||||
@ -481,20 +533,13 @@ namespace
|
||||
extenWindow->showNormal();
|
||||
}
|
||||
|
||||
void ViewThread(int index)
|
||||
{
|
||||
ui.ttCombo->setCurrentIndex(index);
|
||||
ui.textOutput->setPlainText(sanitize(S((current = &Host::GetThread(ParseTextThreadString(ui.ttCombo->itemText(index))))->storage->c_str())));
|
||||
ui.textOutput->moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
void SetOutputFont(QString fontString)
|
||||
{
|
||||
QFont font = ui.textOutput->font();
|
||||
font.fromString(fontString);
|
||||
font.setStyleStrategy(QFont::NoFontMerging);
|
||||
ui.textOutput->setFont(font);
|
||||
QSettings(CONFIG_FILE, QSettings::IniFormat).setValue(FONT, font.toString());
|
||||
Settings().setValue(FONT, font.toString());
|
||||
}
|
||||
|
||||
void ProcessConnected(DWORD processId)
|
||||
@ -555,6 +600,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
|
||||
@ -591,14 +637,14 @@ 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 },
|
||||
{ REMOVE_HOOKS, RemoveHooks },
|
||||
{ SAVE_HOOKS, SaveHooks },
|
||||
{ SEARCH_FOR_HOOKS, FindHooks },
|
||||
{ SETTINGS, Settings },
|
||||
{ SETTINGS, OpenSettings },
|
||||
{ EXTENSIONS, Extensions }
|
||||
})
|
||||
{
|
||||
@ -608,15 +654,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
}
|
||||
ui.processLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding));
|
||||
|
||||
connect(ui.processCombo, qOverload<const QString&>(&QComboBox::currentIndexChanged), [](QString process)
|
||||
{
|
||||
selectedProcessId = ui.processCombo->currentText().split(":")[0].toULong(nullptr, 16);
|
||||
});
|
||||
connect(ui.processCombo, qOverload<int>(&QComboBox::currentIndexChanged), [] { selectedProcessId = ui.processCombo->currentText().split(":")[0].toULong(nullptr, 16); });
|
||||
connect(ui.ttCombo, qOverload<int>(&QComboBox::activated), this, ViewThread);
|
||||
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();
|
||||
@ -640,32 +683,16 @@ 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::wstring::npos) Host::InjectProcess(processId);
|
||||
if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::string::npos) Host::InjectProcess(processId);
|
||||
|
||||
std::thread([]
|
||||
{
|
||||
for (; ; Sleep(10000))
|
||||
{
|
||||
std::unordered_set<std::wstring> attachTargets;
|
||||
if (autoAttach)
|
||||
for (auto process : QString(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process));
|
||||
if (autoAttachSavedOnly)
|
||||
for (auto process : QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
|
||||
attachTargets.insert(S(process.split(" , ")[0]));
|
||||
|
||||
if (!attachTargets.empty())
|
||||
for (auto [processId, processName] : GetAllProcesses())
|
||||
if (processName && attachTargets.count(processName.value()) > 0 && alreadyAttached.count(processId) == 0) Host::InjectProcess(processId);
|
||||
}
|
||||
}).detach();
|
||||
std::thread([] { for (; ; Sleep(10000)) AttachSavedProcesses(); }).detach();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
openSettings().setValue(WINDOW, geometry());
|
||||
Settings().setValue(WINDOW, geometry());
|
||||
CleanupExtensions();
|
||||
SetErrorMode(SEM_NOGPFAULTERRORBOX);
|
||||
ExitProcess(0);
|
||||
@ -673,5 +700,5 @@ MainWindow::~MainWindow()
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent*)
|
||||
{
|
||||
QCoreApplication::quit(); // Need to do this to kill any windows that might've been made by extensions
|
||||
QApplication::quit(); // Need to do this to kill any windows that might've been made by extensions
|
||||
}
|
||||
|
@ -77,7 +77,7 @@
|
||||
<widget class="QPlainTextEdit" name="textOutput">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Meiryo UI</family>
|
||||
<family>Arial Unicode MS</family>
|
||||
<pointsize>13</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
|
BIN
INSTALL_THIS_UNICODE_FONT.ttf
Normal file
BIN
INSTALL_THIS_UNICODE_FONT.ttf
Normal file
Binary file not shown.
33
README.md
33
README.md
@ -2,30 +2,28 @@
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker) is an open-source x86/x64 video game text hooker for Windows/Wine based off of [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Watch the [tutorial video](https://tinyurl.com/textractor-tutorial) for a quick rundown on using it.
|
||||
|
||||
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (a.k.a. NextHooker) is an open-source x86/x64 video game text hooker for Windows 7+ (and Wine) based off of [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Watch the [tutorial video](docs/TUTORIAL.md) for a quick rundown on using it.
|
||||
|
||||
## Download
|
||||
|
||||
Releases of Textractor can be found [here](https://github.com/Artikash/Textractor/releases).<br>
|
||||
Official stable releases of Textractor can be found [here](https://github.com/Artikash/Textractor/releases).<br>
|
||||
The last release of ITHVNR can be found [here](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Try running vcredist if you get an error when starting Textractor.
|
||||
Experimental builds of Textractor (with debug info) from the latest source can be found [here](https://ci.appveyor.com/project/Artikash/textractor/history) in the 'Artifacts' section of each job.
|
||||
|
||||
## Features
|
||||
|
||||
- Highly extensible and customizable
|
||||
- Auto hook many game engines (including some not supported by VNR!)
|
||||
- Hook text using /H "hook" codes (most AGTH codes supported)
|
||||
- Directly extract text using /R "read" codes
|
||||
- Automatically search for possible hook codes
|
||||
|
||||
## Support
|
||||
|
||||
Please let me know of any bugs, games that Textractor has trouble hooking, feature requests, or other suggestions.<br>
|
||||
If you have trouble hooking a game please email me a place where I can freely download it, or gift it to me on [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
Let me know of any bugs, games that Textractor has trouble hooking, feature requests, or other suggestions by posting an issue.<br>
|
||||
If you have trouble hooking a game, please show me a way to freely download it or gift it to me on [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Extensions
|
||||
|
||||
@ -34,22 +32,21 @@ See the extensions folder for examples of what extensions can do.
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions are appreciated! Please email (no, I'm not busy!) me at akashmozumdar@gmail.com if you have any questions about the codebase.<br>
|
||||
All contributions are appreciated! Please email me at akashmozumdar@gmail.com if you have any questions about the codebase.<br>
|
||||
You should use the standard process of making a pull request (fork, branch, commit changes, make PR from your branch to my master).<br>
|
||||
Contributing a translation is easy: just translate the strings in text.cpp as well as this README.
|
||||
Contributing a translation is easy: [text.cpp](text.cpp) contains all of the text strings that you need to translate. Translations of this README or the tutorial video transcript are also welcome.
|
||||
|
||||
## Compiling
|
||||
|
||||
Before compiling *Textractor*, you should get Visual Studio with CMake support, as well as Qt version 5.13<br>
|
||||
You should then be able to simply open the folder in Visual Studio, and build. Run Textractor.exe.
|
||||
Before compiling Textractor, you need Qt version 5.13 and Visual Studio with CMake support.
|
||||
Clone Textractor's source and initialize submodules with `git clone https://github.com/Artikash/Textractor.git` and `git submodule update --init`.
|
||||
You should then be able to just open the source folder in Visual Studio and build.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
The host (see GUI/host folder) injects texthook.dll (created from the texthook folder) into the target process and connects to it via 2 pipe files.<br>
|
||||
Host writes to hostPipe, texthook writes to hookPipe.<br>
|
||||
The host injects texthook into the target process and connects to it via 2 pipe files.
|
||||
texthook waits for the pipe to be connected, then injects a few instructions into any text outputting functions (e.g. TextOut, GetGlyphOutline) that cause their input to be sent through the pipe.<br>
|
||||
Additional information about hooks is exchanged via shared memory.<br>
|
||||
The text that the host receives through the pipe is then processed a little before being dispatched back to the GUI.<br>
|
||||
Finally, the GUI dispatches the text to extensions before displaying it.
|
||||
|
||||
## [Developers](CREDITS.md)
|
||||
## [Developers](docs/CREDITS.md)
|
||||
|
52
README_DE.md
Normal file
52
README_DE.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Textractor
|
||||
|
||||
![Wie es aussieht](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.b.a. NextHooker) ist ein open-source x86/x64 Video spiel Text hooker für Windows 7+ (und Wine) basierend auf [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Schau das [Tutorial Video](docs/TUTORIAL.md) (auf Englisch) an für einen schnellen Überblick wie du Textractor verwendest.
|
||||
## Download
|
||||
|
||||
Der offizielle Release ist [hier](https://github.com/Artikash/Textractor/releases) zu finden.<br>
|
||||
Der letzte Release von ITHVNR ist [hier](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Der experimentelle Release von Textractor (mit debug Informationen) ist [hier](https://ci.appveyor.com/project/Artikash/textractor/history) in der 'Artifacts'
|
||||
Kategorie des jeweiligen Jobs.
|
||||
|
||||
## Features
|
||||
|
||||
- Modular und Anpassbar
|
||||
- Automatischen 'hooken' von mehreren Engines (einige davon welche keine VNR Support haben!)
|
||||
- Text 'hooken' mithilfe von /H "hook" Codes (die meisten AGTH codes funktionieren)
|
||||
- Automatische suche nach funktionierenden Hook's
|
||||
|
||||
## Support
|
||||
|
||||
Wenn ihr irgendwelche Fehler, Spiele bei denen Textractor nicht funktioniert, oder Fragen/Anmerkungen habt lasst es mich bitte wissen.<br>
|
||||
Falls ihr Probleme mit einem Spiel habt, schickt mir einen kostenlosen download Link von dem Spiel oder schenkt es mir auf [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Erweiterungen
|
||||
|
||||
Siehe [Example Extension project](https://github.com/Artikash/ExampleExtension) für Anleitungen, wie man eine Erweiterung erstellt.<br>
|
||||
Im 'Extensions' Ordner sind Beispiele für Erweiterungen.
|
||||
|
||||
## Unterstützen
|
||||
|
||||
Ich bin dankbar für alle Unterstützungen! Schickt mir eine E-Mail an akashmozumdar@gmail.com falls ihr Fragen zur Codebasis habt.<br>
|
||||
Verwendet bitte als Standard eine pull Request (fork, branch, commit) zum Master Release.<br>
|
||||
Zu Übersetzungen beizutragen ist einfach: [text.cpp](text.cpp) enthält alle text Strings welche übersetzt werden sollen. Übersetzungen der README oder des Tutorial Video Transkripts sind ebenfalls willkommen.
|
||||
|
||||
## Compiling
|
||||
Zum Compilen braucht ihr Qt Version 5.13 und Visual Studio mit CMake Unterstützung.
|
||||
Erstellt einen Clone vom Quellcode und initialisiert die submodule mit 'git clone https://github.com/Artikash/Textractor.git' und 'git submodule update --init'.
|
||||
Ihr solltet danach in der Lage sein, den Quellordner in Visual Studio zu öffnen und anzufangen.
|
||||
|
||||
## Projekt Architektur
|
||||
|
||||
Der Host injiziert texthooks in den ziel Prozess und verbindet ihn mit 2 Pipe Dateien.
|
||||
Texthook wartet auf die Pipe und injiziert Instruktionen für den Text Output (z.b. TextOut, GetGlyphOutline) welche durch die Pipe gesendet werden.<br>
|
||||
Weitere Informationen werden durch geteilten Speicher ausgetauscht.<br>
|
||||
Der Text, welchen der Host durch die Pipe erhält, wird dann verarbeitet, bevor er wieder an die GUI gesendet wird.<br>
|
||||
Zu guter Letzt, sendet die GUI den Text an die Erweiterung, welche einen lesbaren Output anzeigt.
|
||||
|
||||
## [Entwickler](docs/CREDITS.md)
|
@ -1,12 +1,12 @@
|
||||
# Textractor
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
## [Video tutorial](https://tinyurl.com/textractor-tutorial)
|
||||
## [Video tutorial](docs/TUTORIAL.md)
|
||||
|
||||
## Descripción general
|
||||
|
||||
**Textractor** (también conocido como NextHooker) es un extractor y traductor de texto de código abierto x86/x64 para Windows/Wine bassado en [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
**Textractor** (también conocido como NextHooker) es un extractor y traductor de texto de código abierto x86/x64 para Windows/Wine bassado en [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
|
||||
![Cómo se ve](screenshot.png)
|
||||
|
||||
|
51
README_FR.md
Normal file
51
README_FR.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Textractor
|
||||
|
||||
![À quoi ça ressemble](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker) est un traducteur de jeux-videos basé surtout sur du texte en open source x86/x64 pour Windows/Wine [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Regarde le [tutorial video](docs/TUTORIAL.md) pour un aperçu rapide de son utilisation.
|
||||
|
||||
## Téléchargement
|
||||
|
||||
Les versions de Textractor peuvent être trouvées[here](https://github.com/Artikash/Textractor/releases).<br>
|
||||
Le denière version de THVNR peut etre trouvé [here](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Nouveautés
|
||||
- Hautement extensible et personnalisable
|
||||
- Accrochage automatique de nombreux moteurs de jeu (dont certains non pris en charge par VNR!)
|
||||
- Les textes de hook qui utilisent des /H "hook" codes (la plupart des codes AGTH pris en charge)
|
||||
- Extraire directement le texte à l'aide des codes /R "lire"
|
||||
|
||||
## Support
|
||||
|
||||
Veuillez me signaler les bugs, les jeux dont Textractor a du mal à accrocher, les demandes de fonctionnalités ou d'autres suggestions. <br>
|
||||
Si vous rencontrez des difficultés pour accrocher un jeu, veuillez m'envoyer un e-mail à un endroit où je peux le télécharger librement/gratuitement ou me le faire cadeau sur [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Extensions
|
||||
|
||||
Regarde mes [Example Extension project](https://github.com/Artikash/ExampleExtension) pour voir comment créer un extension.<br>
|
||||
Voir le dossier des extensions pour des exemples de ce que les extensions peuvent faire.
|
||||
|
||||
## Contribution
|
||||
|
||||
Toutes les contributions sont appréciées! Veuillez m'envoyer un e-mail À akashmozumdar@gmail.com si vous avez des questions sur la base de code. <br>
|
||||
Vous devez utiliser le processus standard de création d'une demande d'extraction (fork, branch, commit changes, make PR from your branch to my master). <br>
|
||||
Contribuer à une traduction est simple: il suffit de traduire les chaînes dans [text.cpp](text.cpp) ainsi que ce fichier README.
|
||||
|
||||
## Compilation
|
||||
|
||||
Avant de compiler *Textractor*, vous devriez obtenir Visual Studio avec prise en charge de CMake, ainsi que Qt version 5.13 <br>
|
||||
Vous devriez ensuite pouvoir simplement ouvrir le dossier dans Visual Studio et faire le build. Exécutez Textractor.exe.
|
||||
|
||||
## Architecture du projet
|
||||
|
||||
L'hôte (voir GUI / dossier hôte) injecte texthook.dll (créé à partir du dossier texthook) dans le processus cible et s'y connecte via 2 fichiers pipe. <br>
|
||||
L'hôte écrit dans hostPipe, texthook écrit dans hookPipe. <br>
|
||||
Texthook attend que le canal soit connecté, puis injecte quelques instructions dans toutes les fonctions de sortie de texte (par exemple TextOut, GetGlyphOutline) qui provoquent l'envoi de leur entrée via le canal. <br>
|
||||
Des informations supplémentaires sur les hooks sont échangées via la mémoire partagée. <br>
|
||||
Le texte que l'hôte reçoit via le canal est ensuite traité un peu avant d'être renvoyé à l'interface graphique. <br>
|
||||
Enfin, l'interface graphique envoie le texte aux extensions avant de l'afficher.
|
||||
|
||||
## [Développeurs](docs/CREDITS.md)
|
40
README_ID.md
40
README_ID.md
@ -2,18 +2,15 @@
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a NextHooker) adalah text hooker video game untuk Windows/Wine x86/x64 berbasis open-source yang didasari oleh [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Lihat [video tutorial](https://tinyurl.com/textractor-tutorial) untuk mengetahui bagaimana cara menggunakannya.
|
||||
|
||||
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (a.k.a NextHooker) adalah teks hooker video game untuk Windows/Wine x86/x64 berbasis open-source yang didasari oleh [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Lihat [video tutorial](docs/TUTORIAL.md) untuk mengetahui bagaimana cara menggunakannya.
|
||||
|
||||
## Pengunduhan
|
||||
|
||||
Rilisan Textractor dapat diunduh [disini](https://github.com/Artikash/Textractor/releases).<br>
|
||||
Rilisan Terakhir ITHVNR dapat diunduh [disini](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Coba jalankan vcredist.x86.exe jika kamu memiliki masalah ketika menjalankan Textractor.
|
||||
Rilisan Terakhir ITHVNR dapat diunduh [disini](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Fitur
|
||||
|
||||
@ -24,36 +21,33 @@ Coba jalankan vcredist.x86.exe jika kamu memiliki masalah ketika menjalankan Tex
|
||||
|
||||
## Dukungan
|
||||
|
||||
Please let me know of any bugs, games that Textractor has trouble hooking, feature requests, or other suggestions.<br>
|
||||
If you have trouble hooking a game please email me a place where I can freely download it, or gift it to me on [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
Tolong beritahu saya jika kamu menemukan bug, game yang tidak dapat di tempel oleh Textractor, permintaan fitur, atau usulan lain.<br>
|
||||
Jika kamu memiliki masalah dalam menempelkan kedalam game tolong email saya link agar saya dapat mendownload game tersebut, atau gift game tersebut di [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
Tolong beritahu saya jika kamu menemukan kutu, game yang tidak dapat di tempel oleh Textractor, permintaan fitur, atau usulan lain.<br>
|
||||
Jika kamu memiliki masalah dalam menempelkan kedalam game tolong email saya link agar saya dapat mengunduh game tersebut, atau hadiahkan game tersebut di [Steam](https://steamcommunity.com/profiles/76561198097566313/).
|
||||
|
||||
## Ekstensi
|
||||
|
||||
Lihat [project sampel ekstensi saya](https://github.com/Artikash/ExampleExtension) untuk melihat bagaimana cara membuat ekstensi.<br>
|
||||
Lihat folder extensions untuk melihat sampel ekstensi.
|
||||
Lihat ekstensi folder untuk melihat sampel ekstensi.
|
||||
|
||||
## Kontribusi
|
||||
|
||||
Seluruh kontribusi diapresiasi! Tolong email (tidak, saya tidak sibuk!) saya di akashmozumdar@gmail.com jika kamu memiliki pertanyaan mengenai codebase nya.<br>
|
||||
Kamu harus menggunakan proses standar dalam membuat permintaan pull(fork, branch, commit changes, membuat PR dari branch kamu ke master saya).<br>
|
||||
Berkontribusi dalam penerjemahan dapat dilakukan dengan mudah : cukup terjemahkan string dari text.cpp lalu terjemahkan README ini.
|
||||
Seluruh kontribusi diapresiasi! Tolong email saya di akashmozumdar@gmail.com jika kamu memiliki pertanyaan mengenai kode dasar nya.<br>
|
||||
Kamu harus menggunakan proses standar dalam membuat permintaan pull(fork, cabang, perubahan commit, membuat PR dari cabang kamu ke master saya).<br>
|
||||
Berkontribusi dalam penerjemahan dapat dilakukan dengan mudah : cukup terjemahkan string dari [text.cpp](text.cpp) lalu terjemahkan README ini.
|
||||
|
||||
## Compiling
|
||||
## Mengcompile
|
||||
|
||||
Sebelum melakukan proses compile *Textractor*, kamu harus memiliki Visual Studio dengan dukungan Cmake, juga dengan Qt version 5.13<br>
|
||||
Lalu kamu dapat membuka folder di Visual Studio, dan build. Run Textractor.exe.
|
||||
Lalu kamu dapat membuka folder di Visual Studio, dan build. Jalankan Textractor.exe.
|
||||
|
||||
|
||||
## Arsitektur Project
|
||||
|
||||
Host (lihat folder GUI/host) menginject texthook.dll (dibuat dari folder texthook) kedalam target process dan disambungkan lewat 2 file pipe.<br>
|
||||
Host (lihat folder host) menyuntikan texthook.dll (dibuat dari folder texthook) kedalam target proses dan disambungkan lewat 2 file pipe.<br>
|
||||
Host menulis ke hostPipe, texthook menulis ke hookPipe.<br>
|
||||
texthook menunggu pipe tersambung, lalu menginject beberapa instruksi ke teks yang menghasilkan fungsi (contoh: TextOut, GetGlyphOutline) yang membuat input dikirim melewati pipa.<br>
|
||||
texthook menunggu pipe tersambung, lalu menyuntikan beberapa instruksi ke teks yang menghasilkan fungsi (contoh: TextOut, GetGlyphOutline) yang membuat input dikirim melewati pipa.<br>
|
||||
Informasi tambahan tentang hook dipindahkan melewati shared memory.<br>
|
||||
Text yang diterima host melewati pipe lalu diproses lagi sebelum dikembalikan ke GUI.<br>
|
||||
Dan pada akhirnya, GUI melepas text ke ekstensi sebelum menampilkan teks.
|
||||
Teks yang diterima host melewati pipe lalu diproses lagi sebelum dikembalikan ke GUI.<br>
|
||||
Dan pada akhirnya, GUI melepas teks ke ekstensi sebelum menampilkan teks.
|
||||
|
||||
## [Pengembang](CREDITS.md)
|
||||
## [Pengembang](docs/CREDITS.md)
|
||||
|
17
README_IT.md
17
README_IT.md
@ -2,18 +2,15 @@
|
||||
|
||||
![Come si vede](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker) è un agganciatore di testi di videogiochi open-source per Windows/Wine basato su[ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Guarda il [video tutorial](https://tinyurl.com/textractor-tutorial) per una sintesi veloce sul suo utilizzo.
|
||||
|
||||
[![Donazioni](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (a.k.a. NextHooker) è un agganciatore di testi di videogiochi open-source per Windows/Wine basato su[ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Guarda il [video tutorial](docs/TUTORIAL.md) per una sintesi veloce sul suo utilizzo.
|
||||
|
||||
## Scarica
|
||||
|
||||
Le uscite di Textractor possono essere trovate [qui](https://github.com/Artikash/Textractor/releases).<br>
|
||||
L'ultima uscita di ITHVNR può essere trovata [qui](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Prova ad avviare vcredist se ti esce un errore quando avvi Textractor.
|
||||
L'ultima uscita di ITHVNR può essere trovata [qui](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Caratteristiche
|
||||
|
||||
@ -36,7 +33,7 @@ Guardate la cartella delle estenzioni per esempi di cosa possono fare le estenzi
|
||||
|
||||
Tutti i contributi sono apprezzati! Inviatemi un email a akashmozumdar@gmail.com se avete delle domande sul codebase.<br>
|
||||
Dovreste usare il processo standard di creare una pull request (fork, branch, commit changes, crea PR dal vostro ramo al mio master).<br>
|
||||
Contribuire alla traduzione è semplice: traduci le stringhe in text.cpp cosi come questo README.
|
||||
Contribuire alla traduzione è semplice: traduci le stringhe in [text.cpp](text.cpp) cosi come questo README.
|
||||
|
||||
## Compiling
|
||||
|
||||
@ -45,11 +42,11 @@ Dovresti essere in grado di aprire la cartella in Visual Studio, e costruire. Av
|
||||
|
||||
## Architettura del progetto
|
||||
|
||||
L'host (guarda la cartella GUI/host) innietta texthook.dll (creato dalla cartella texthook) nel processo e lo connette attraverso due file pipe.<br>
|
||||
L'host (guarda la cartella host) innietta texthook.dll (creato dalla cartella texthook) nel processo e lo connette attraverso due file pipe.<br>
|
||||
L'host scrive a hostPipe, texthook scrive a hookPipe.<br>
|
||||
Texthook aspetta per il pipe di essere connesso, poi innietta alcune istruzione in qualunque funzione di immissione del testo (es. TextOut, GetGlyphOutline) che causa il loro input di essere inviato attraverso il pipe.<br>
|
||||
Informazioni aggiuntive sui ganci soo scambiati attraverso la memorio condivisa.<br>
|
||||
Il testo che l'host riceve attraverso il pipe è poi processato un poco prima di essere rinviato alla GUI.<br>
|
||||
Infine, la GUI dispone il testo alle estenzioni prima di mostrarle.
|
||||
|
||||
## [Sviluppatori](CREDITS.md)
|
||||
## [Sviluppatori](docs/CREDITS.md)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Textractor
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** はビジュアルノベル文字抽出プログラム。
|
||||
|
||||
|
15
README_KR.md
15
README_KR.md
@ -2,18 +2,15 @@
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker)는 Windows/Wine에서 작동하는 [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine)을 기반으로 한 오픈소스 x86/x64 비디오게임 텍스트 후커 입니다.<br>
|
||||
빠른 사용법의 이해를 위해 [tutorial video](https://tinyurl.com/textractor-tutorial) 를 참고하세요.
|
||||
|
||||
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (a.k.a. NextHooker)는 Windows/Wine에서 작동하는 [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine)을 기반으로 한 오픈소스 x86/x64 비디오게임 텍스트 후커 입니다.<br>
|
||||
빠른 사용법의 이해를 위해 [tutorial video](docs/TUTORIAL.md) 를 참고하세요.
|
||||
|
||||
## 다운로드
|
||||
|
||||
[여기](https://github.com/Artikash/Textractor/releases)에서 Textractor 최신버전을 받으실 수 있습니다.<br>
|
||||
최신버전의 ITHVNR은 [여기](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO)서 받을 수 있습니다.<br>
|
||||
Textractor 실행오류를 겪는다면 vcredist를 실행해 보시기 바랍니다.
|
||||
최신버전의 ITHVNR은 [여기](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO)서 받을 수 있습니다.
|
||||
|
||||
## 특징
|
||||
|
||||
@ -43,11 +40,11 @@ Textractor 실행오류를 겪는다면 vcredist를 실행해 보시기 바랍
|
||||
|
||||
## 프로젝트 아키텍쳐
|
||||
|
||||
The host (see GUI/host folder) injects texthook.dll (created from the texthook folder) into the target process and connects to it via 2 pipe files.<br>
|
||||
The host (see host folder) injects texthook.dll (created from the texthook folder) into the target process and connects to it via 2 pipe files.<br>
|
||||
Host writes to hostPipe, texthook writes to hookPipe.<br>
|
||||
texthook waits for the pipe to be connected, then injects a few instructions into any text outputting functions (e.g. TextOut, GetGlyphOutline) that cause their input to be sent through the pipe.<br>
|
||||
Additional information about hooks is exchanged via shared memory.<br>
|
||||
The text that the host receives through the pipe is then processed a little before being dispatched back to the GUI.<br>
|
||||
Finally, the GUI dispatches the text to extensions before displaying it.
|
||||
|
||||
## [개발자들](CREDITS.md)
|
||||
## [개발자들](docs/CREDITS.md)
|
||||
|
19
README_PT.md
19
README_PT.md
@ -2,18 +2,15 @@
|
||||
|
||||
![Como se Parece](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (também conhecido como NextHooker) é um extrator de textos de video-games x86/x64 para Windows/Wine baseado no [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Assista ao [vídeo tutorial](https://tinyurl.com/textractor-tutorial) para uma rápida apresentação de como utilizá-lo.
|
||||
|
||||
[![Doe](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (também conhecido como NextHooker) é um extrator de textos de video-games x86/x64 para Windows/Wine baseado no [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Assista ao [vídeo tutorial](docs/TUTORIAL.md) para uma rápida apresentação de como utilizá-lo.
|
||||
|
||||
## Download
|
||||
|
||||
As versões lançadas podem ser encontradas [aqui](https://github.com/Artikash/Textractor/releases).<br>
|
||||
A última versão lançada do ITHVNR pode ser encontrada [aqui](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Tente rodar o vcredist se você encontrar algum erro ao iniciar o Textractor.
|
||||
A última versão lançada do ITHVNR pode ser encontrada [aqui](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Recursos e Funções
|
||||
|
||||
@ -34,9 +31,9 @@ Veja a pasta de extensões para mais exemplos do que as extensões são capazes
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Todas contribuições são bem-vindas! Por favor, me mande um e-mail (não, não sou ocupado!) no endereço akashmozumdar@gmail.com caso tenha alguma dúvida quanto ao codebase.<br>
|
||||
Todas contribuições são bem-vindas! Por favor, me mande um e-mail no endereço akashmozumdar@gmail.com caso tenha alguma dúvida quanto ao codebase.<br>
|
||||
Você deve seguir o processo padrão de fazer um pull request (fork, branch, realizar mudanças, realizar o PR do seu branch para o meu master).<br>
|
||||
Contribuir com uma tradução é fácil: basta traduzir as linhas do text.cpp assim como esse README.
|
||||
Contribuir com uma tradução é fácil: basta traduzir as linhas do [text.cpp](text.cpp) assim como esse README.
|
||||
|
||||
## Compilando
|
||||
|
||||
@ -45,11 +42,11 @@ Você deverá então ser capaz de simplesmente abrir uma pasta no Visual Studio
|
||||
|
||||
## Arquitetura do Projeto
|
||||
|
||||
O host (veja a pasta GUI/host) injeta o texthook.dll (criado a partir da pasta texthook) dentro do processo-alvo e se conecta a ele por meio de 2 arquivos pipe.<br>
|
||||
O host (veja a pasta host) injeta o texthook.dll (criado a partir da pasta texthook) dentro do processo-alvo e se conecta a ele por meio de 2 arquivos pipe.<br>
|
||||
O Host escreve para hostPipe, o texthook escreve para hookPipe.<br>
|
||||
O texthook espera pelo pipe estar conectado e então injeta algumas intruções dentro de quaisquer funções que produzam texto (por exemplo: TextOut, GetGlyphOutline) o que faz com que seu produto seja mandado por meio do pipe.<br>
|
||||
Informação adicional sobre os hooks é trocada por meio da memória compartilhada.<br>
|
||||
O texto que o host recebe por meio do pipe é então processado um pouco antes de ser despachado devolta para a IGU/GUI.<br>
|
||||
Finalmente, a IGU/GUI despacha o texto para as extensões antes de mostrá-lo.
|
||||
|
||||
## [Desenvolvedores](CREDITS.md)
|
||||
## [Desenvolvedores](docs/CREDITS.md)
|
||||
|
19
README_RU.md
19
README_RU.md
@ -2,18 +2,15 @@
|
||||
|
||||
![Как это выглядит](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor** (a.k.a. NextHooker) это проект x86/x64 Windows/Wine программы для захвата текста из видеоигр, основанный на [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Смотреть [обучающее видео](https://tinyurl.com/textractor-tutorial) для быстрого ознакомления.
|
||||
|
||||
[![Задонатить автору](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
**Textractor** (a.k.a. NextHooker) это проект x86/x64 Windows/Wine программы для захвата текста из видеоигр, основанный на [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
Смотреть [обучающее видео](docs/TUTORIAL.md) для быстрого ознакомления.
|
||||
|
||||
## Загрузка
|
||||
|
||||
Выпуски Textractor могут быть найдены [здесь](https://github.com/Artikash/Textractor/releases).<br>
|
||||
Последний выпуск ITHVNR может быть найден [здесь](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
Установите библиотеки Visual C redist(vcredist.x86.exe), если получаете ошибку при запуске Textractor.
|
||||
Последний выпуск ITHVNR может быть найден [здесь](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## Возможности
|
||||
|
||||
@ -34,9 +31,9 @@
|
||||
|
||||
## Вклад
|
||||
|
||||
Любой вклад приветствуется! Пишите мне(автору)(нет, я не занят!) на akashmozumdar@gmail.com, если у вас есть любые вопросы о кодовой базе.<br>
|
||||
Любой вклад приветствуется! Пишите мне(автору) на akashmozumdar@gmail.com, если у вас есть любые вопросы о кодовой базе.<br>
|
||||
Используйте стандартные действия для создания pull request (fork, branch, commit changes, создайте PR из своей ветки branch на мой master).<br>
|
||||
Вклад в перевод совсем не сложен: просто переведите строки в text.cpp, также, как и этот README.
|
||||
Вклад в перевод совсем не сложен: просто переведите строки в [text.cpp](text.cpp), также, как и этот README.
|
||||
|
||||
## Компиляция
|
||||
|
||||
@ -45,11 +42,11 @@
|
||||
|
||||
## Архитектура проекта
|
||||
|
||||
Хост (смотрите папку GUI/host) внедряет texthook.dll (созданной из папки texthook) в целевой процесс и подключается к нему через два файла-канала (pipe).<br>
|
||||
Хост (смотрите папку host) внедряет texthook.dll (созданной из папки texthook) в целевой процесс и подключается к нему через два файла-канала (pipe).<br>
|
||||
Хост пишет в hostPipe, texthook пишет в hookPipe.<br>
|
||||
texthook ждет присоединения канала, тогда внедряет некоторые инструкции в любые выводящие текст функции (такие как TextOut, GetGlyphOutline), что вызывает пересылку поступающего в них текста через канал.<br>
|
||||
Дополнительная информация о хуках размещена через файл просмотра (a.k.a. section object), который сопоставлен с ссылкой на класс TextHook.<br>
|
||||
Текст, который хост получает через канал, затем немного обрабатывается перед отправкой обратно в графический интерфейс (GUI).<br>
|
||||
Наконец, GUI отправляет текст расширениям, перед его отображением.
|
||||
|
||||
## [Разработчики](CREDITS.md)
|
||||
## [Разработчики](docs/CREDITS.md)
|
||||
|
12
README_SC.md
12
README_SC.md
@ -1,10 +1,10 @@
|
||||
# Textractor
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
## 概述
|
||||
|
||||
**Textractor** (曾用名: NextHooker) 是一个基于 [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine), 为 Windows/Wine 开发的开源 x86/x64 文本提取器.<br>
|
||||
**Textractor** (曾用名: NextHooker) 是一个基于 [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine), 为 Windows/Wine 开发的开源 x86/x64 文本提取器.<br>
|
||||
|
||||
![它工作起来的样子](screenshot.png)
|
||||
|
||||
@ -28,9 +28,9 @@ Textractor 的发行版可以在[这里](https://github.com/Artikash/Textractor/
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎一切贡献!如有任何关于代码的疑问,请向 akashmozumdar@gmail.com 发邮件 (不,我并不忙!).<br>
|
||||
欢迎一切贡献!如有任何关于代码的疑问,请向 akashmozumdar@gmail.com 发邮件.<br>
|
||||
你应当使用创建 PR 的标准过程 (分岔 (fork), 分支 (branch), 提交变化, 创建从你的分支到我的 master 分支的 PR).<br>
|
||||
提供翻译贡献很简单: 只需翻译 text.cpp 中的字符串和这份 README 即可.
|
||||
提供翻译贡献很简单: 只需翻译 [text.cpp](text.cpp) 中的字符串和这份 README 即可.
|
||||
|
||||
## 编译
|
||||
|
||||
@ -39,11 +39,11 @@ Textractor 的发行版可以在[这里](https://github.com/Artikash/Textractor/
|
||||
|
||||
## 项目架构
|
||||
|
||||
宿主 (位于 GUI/host 文件夹) 向目标进程注入 texthook.dll (由 texthook 文件夹创建) 并通过两个管道文件互联.<br>
|
||||
宿主 (位于 host 文件夹) 向目标进程注入 texthook.dll (由 texthook 文件夹创建) 并通过两个管道文件互联.<br>
|
||||
宿主向 hostPipe 写入, texthook 向 hookPipe 写入.<br>
|
||||
texthook 等待管道连接, 之后向一些文本输出函数 (如 TextOut, GetGlyphOutline) 注入一系列指令, 使得它们的输入被沿着管道发送.<br>
|
||||
其它关于钩子的信息通过一个被 TextHook 类保有引用的文件视图 (曾用名: 段对象) 共享.<br>
|
||||
之后, 宿主通过管道接收到的文本在传回 GUI 前被简单处理.<br>
|
||||
最后, GUI 在显示文本前将其分发给扩展.
|
||||
|
||||
## [开发者](CREDITS.md)
|
||||
## [开发者](docs/CREDITS.md)
|
||||
|
15
README_TH.md
15
README_TH.md
@ -2,19 +2,16 @@
|
||||
|
||||
![How it looks](screenshot.png)
|
||||
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md)
|
||||
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [Русский](README_RU.md) ● [한국어](README_KR.md) ● [ภาษาไทย](README_TH.md) ● [Français](README_FR.md) ● [Italiano](README_IT.md) ● [日本語](README_JP.md) ● [Bahasa Indonesia](README_ID.md) ● [Português](README_PT.md) ● [Deutsch](README_DE.md)
|
||||
|
||||
**Textractor**
|
||||
(หรือ NextHooker) คือโปรแกรมโอเพนซอร์ซสำหรับปฏิบัติการที่มีหน้าที่เพื่อเชื่อมกับตัวอักษรกับเกมจากที่มาจากระบบปฏิบัติการ Window/Wine โดยมีแบบดังเดิมมาจาก [ITHVNR](http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
สามารถดูตัวอย่างวิธีการใช้งาน [วีดีโอตัวอย่างการใช้งาน](https://tinyurl.com/textractor-tutorial) เพื่อที่จะแสดงความเข้าใจคร่าวๆเกี่ยวกับโปรแกรม.
|
||||
|
||||
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=akashmozumdar%40gmail.com&item_name=Textractor%20development¤cy_code=USD)
|
||||
(หรือ NextHooker) คือโปรแกรมโอเพนซอร์ซสำหรับปฏิบัติการที่มีหน้าที่เพื่อเชื่อมกับตัวอักษรกับเกมจากที่มาจากระบบปฏิบัติการ Window/Wine โดยมีแบบดังเดิมมาจาก [ITHVNR](https://web.archive.org/web/20160202084144/http://www.hongfire.com/forum/showthread.php/438331-ITHVNR-ITH-with-the-VNR-engine).<br>
|
||||
สามารถดูตัวอย่างวิธีการใช้งาน [วีดีโอตัวอย่างการใช้งาน](docs/TUTORIAL.md) เพื่อที่จะแสดงความเข้าใจคร่าวๆเกี่ยวกับโปรแกรม.
|
||||
|
||||
## ดาวน์โหลด
|
||||
|
||||
Textractor รุ่นล่าสุดสามารถดาวน์โหลดจาก [ที่นี้](https://github.com/Artikash/Textractor/releases).<br>
|
||||
ITHVNR รุ่นสุดท้ายสามารถดาวน์โหลดได้ [ที่นี้](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).<br>
|
||||
ถ้าหากมีปัญหาขณะที่เปิด Textractor ลองเปิด vcredist
|
||||
ITHVNR รุ่นสุดท้ายสามารถดาวน์โหลดได้ [ที่นี้](https://drive.google.com/open?id=13aHF4uIXWn-3YML_k2YCDWhtGgn5-tnO).
|
||||
|
||||
## คุณสมบัติ
|
||||
|
||||
@ -40,7 +37,7 @@ ITHVNR รุ่นสุดท้ายสามารถดาวน์โห
|
||||
|
||||
## โครงสร้างโปรแกรม
|
||||
|
||||
ฐานของโปรแกรม (โฟลเดอร์ GUI/host) ส่งข้อมูลจาก texthook.dll (ที่ถูกสร้างจาก texthook โฟลเดอร์) ไปยังเกมเป้าหมาย และ เชื่อมทั่งสองอย่างเข้าด้วยกัน<br>
|
||||
ฐานของโปรแกรม (โฟลเดอร์ host) ส่งข้อมูลจาก texthook.dll (ที่ถูกสร้างจาก texthook โฟลเดอร์) ไปยังเกมเป้าหมาย และ เชื่อมทั่งสองอย่างเข้าด้วยกัน<br>
|
||||
ฐานโปรแกรมเขียนผ่านฝั่ง hostPipe(ท่อเชื่อมฝั่งฐานข้อมูล) ในขณะที่ตัวดึงตัวอักษรที่ทางฝั่ง hookPipe(ท่อเชื่อมฝั่งดึงข้อมูล).<br>
|
||||
ตัวดึงตัวอักษรรอการเชื่อมเข้ากับของทั่งสองท่อ หลังจากนั่นส่งคำสั่งไปยังข้อมูลนั่น (เช่น แสดงผลข้อมูล เป็นต้น) และทำให้ข้อมูลส่งผ่านต่อมาออกมาได้ถูกต้อง<br>
|
||||
ข้อมูลบางอย่างเกี่ยวกับการเชื่อมจะถูกแลกเปลี่ยนผ่านความทรงจำของระบบ (shared memory)
|
||||
@ -48,4 +45,4 @@ ITHVNR รุ่นสุดท้ายสามารถดาวน์โห
|
||||
ตัวอักษรที่ฐานโปรแกรมรับผ่านท่อจะถูกแปลงเล็กน้อยก่อนที่จะแสดงผ่าน GUI <br>
|
||||
สุดท้ายแล้ว GUI จะส่งข้อมูลตัวอักษรไปยังส่วนขยายต่างๆก่อนที่จะแสดงให้เห็นในหน้าจอ
|
||||
|
||||
## [นักพัฒนา](CREDITS.md)
|
||||
## [นักพัฒนา](docs/CREDITS.md)
|
||||
|
@ -1,109 +1,84 @@
|
||||
macro(msvc_registry_search)
|
||||
IF(MSVC)
|
||||
# look for user-registry pointing to qtcreator
|
||||
get_filename_component(QT_BIN [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\QtProject.QtCreator.pro\\shell\\Open\\Command] PATH)
|
||||
if(NOT DEFINED Qt5_DIR)
|
||||
if (NOT EXISTS ${QT_ROOT})
|
||||
# look for user-registry pointing to qtcreator
|
||||
get_filename_component(QT_ROOT [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\QtProject.QtCreator.pro\\shell\\Open\\Command] PATH)
|
||||
|
||||
# get root path so we can search for 5.3, 5.4, 5.5, etc
|
||||
string(REPLACE "/Tools" ";" QT_BIN "${QT_BIN}")
|
||||
list(GET QT_BIN 0 QT_BIN)
|
||||
file(GLOB QT_VERSIONS "${QT_BIN}/5.*")
|
||||
list(SORT QT_VERSIONS)
|
||||
# get root path
|
||||
string(REPLACE "/Tools" ";" QT_ROOT "${QT_ROOT}")
|
||||
list(GET QT_ROOT 0 QT_ROOT)
|
||||
endif()
|
||||
file(GLOB QT_VERSIONS "${QT_ROOT}/5.13*")
|
||||
list(SORT QT_VERSIONS)
|
||||
|
||||
# assume the latest version will be last alphabetically
|
||||
list(REVERSE QT_VERSIONS)
|
||||
# assume the latest version will be last alphabetically
|
||||
list(REVERSE QT_VERSIONS)
|
||||
|
||||
list(GET QT_VERSIONS 0 QT_VERSION)
|
||||
list(GET QT_VERSIONS 0 QT_VERSION)
|
||||
|
||||
# fix any double slashes which seem to be common
|
||||
string(REPLACE "//" "/" QT_VERSION "${QT_VERSION}")
|
||||
# fix any double slashes which seem to be common
|
||||
string(REPLACE "//" "/" QT_VERSION "${QT_VERSION}")
|
||||
|
||||
if(MSVC_VERSION GREATER_EQUAL "1910")
|
||||
set(QT_MSVC "2017")
|
||||
elseif(MSVC_VERSION GREATER_EQUAL "1900")
|
||||
set(QT_MSVC "2015")
|
||||
else()
|
||||
# Latest QT versions >5.10 provides only 2015 and 2017 prebuilt binaries
|
||||
message(WARNING "Unsupported MSVC toolchain version")
|
||||
endif()
|
||||
if(MSVC_VERSION GREATER_EQUAL 1920)
|
||||
set(QT_MSVC 2019)
|
||||
elseif(MSVC_VERSION GREATER_EQUAL 1910)
|
||||
set(QT_MSVC 2017)
|
||||
elseif(MSVC_VERSION GREATER_EQUAL 1900)
|
||||
set(QT_MSVC 2015)
|
||||
else()
|
||||
message(WARNING "Unsupported MSVC toolchain version")
|
||||
endif()
|
||||
|
||||
if(QT_MSVC)
|
||||
# check for 64-bit target
|
||||
if(CMAKE_CL_64)
|
||||
SET(QT_MSVC "${QT_MSVC}_64")
|
||||
endif()
|
||||
if(QT_MSVC)
|
||||
if(CMAKE_CL_64)
|
||||
SET(QT_SUFFIX "_64")
|
||||
else()
|
||||
set(QT_SUFFIX "")
|
||||
endif()
|
||||
|
||||
set(QT_TOOLCHAIN "${QT_VERSION}/msvc${QT_MSVC}")
|
||||
if(EXISTS ${QT_TOOLCHAIN})
|
||||
set(Qt5_DIR "${QT_TOOLCHAIN}/lib/cmake/Qt5")
|
||||
elseif(QT_MSVC EQUAL "2017")
|
||||
#2017 is ABI compatible with 2015
|
||||
if(CMAKE_CL_64)
|
||||
set(QT_TOOLCHAIN "${QT_VERSION}/msvc2015_64")
|
||||
else()
|
||||
set(QT_TOOLCHAIN "${QT_VERSION}/msvc2015")
|
||||
endif()
|
||||
|
||||
if(EXISTS ${QT_TOOLCHAIN})
|
||||
set(Qt5_DIR "${QT_TOOLCHAIN}/lib/cmake/Qt5")
|
||||
else()
|
||||
message(WARNING "Required QT5 toolchain is not installed")
|
||||
endif()
|
||||
else()
|
||||
message(WARNING "Required QT5 toolchain is not installed")
|
||||
endif()
|
||||
endif()
|
||||
ENDIF()
|
||||
# MSVC 2015+ is only backwards compatible
|
||||
if(EXISTS "${QT_VERSION}/msvc${QT_MSVC}${QT_SUFFIX}")
|
||||
set(Qt5_DIR "${QT_VERSION}/msvc${QT_MSVC}${QT_SUFFIX}/lib/cmake/Qt5")
|
||||
elseif(QT_MSVC GREATER_EQUAL 2019 AND EXISTS "${QT_VERSION}/msvc2017${QT_SUFFIX}")
|
||||
set(Qt5_DIR "${QT_VERSION}/msvc2017${QT_SUFFIX}/lib/cmake/Qt5")
|
||||
elseif(QT_MSVC GREATER_EQUAL 2017 AND EXISTS "${QT_VERSION}/msvc2015${QT_SUFFIX}")
|
||||
set(Qt5_DIR "${QT_VERSION}/msvc2015${QT_SUFFIX}/lib/cmake/Qt5")
|
||||
else()
|
||||
message(WARNING "Required QT5 toolchain is not installed")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
macro(find_qt5)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
#set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
add_definitions(-DQT_DEPRECATED_WARNINGS -DQT_DISABLE_DEPRECATED_BEFORE=0x060000)
|
||||
find_package(Qt5 COMPONENTS ${ARGN})
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
#set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
#add_definitions(-DQT_DEPRECATED_WARNINGS -DQT_DISABLE_DEPRECATED_BEFORE=0x060000)
|
||||
find_package(Qt5 COMPONENTS ${ARGN})
|
||||
|
||||
if(Qt5_FOUND)
|
||||
if(WIN32 AND TARGET Qt5::qmake AND NOT TARGET Qt5::windeployqt)
|
||||
get_target_property(_qt5_qmake_location Qt5::qmake IMPORTED_LOCATION)
|
||||
if(Qt5_FOUND)
|
||||
if(WIN32 AND TARGET Qt5::qmake AND NOT TARGET Qt5::windeployqt)
|
||||
get_target_property(_qt5_qmake_location Qt5::qmake IMPORTED_LOCATION)
|
||||
|
||||
execute_process(
|
||||
COMMAND "${_qt5_qmake_location}" -query QT_INSTALL_PREFIX
|
||||
RESULT_VARIABLE return_code
|
||||
OUTPUT_VARIABLE qt5_install_prefix
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
execute_process(
|
||||
COMMAND "${_qt5_qmake_location}" -query QT_INSTALL_PREFIX
|
||||
RESULT_VARIABLE return_code
|
||||
OUTPUT_VARIABLE qt5_install_prefix
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
set(imported_location "${qt5_install_prefix}/bin/windeployqt.exe")
|
||||
set(imported_location "${qt5_install_prefix}/bin/windeployqt.exe")
|
||||
|
||||
if(EXISTS ${imported_location})
|
||||
add_executable(Qt5::windeployqt IMPORTED)
|
||||
if(EXISTS ${imported_location})
|
||||
add_executable(Qt5::windeployqt IMPORTED)
|
||||
|
||||
set_target_properties(Qt5::windeployqt PROPERTIES
|
||||
IMPORTED_LOCATION ${imported_location}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Cannot find QT5!")
|
||||
endif()
|
||||
set_target_properties(Qt5::windeployqt PROPERTIES
|
||||
IMPORTED_LOCATION ${imported_location}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
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)
|
||||
|
54
deploy.ps1
54
deploy.ps1
@ -1,8 +1,8 @@
|
||||
param([string]$version)
|
||||
|
||||
cd $PSScriptRoot;
|
||||
mkdir -Force -Verbose Builds;
|
||||
cd Builds;
|
||||
mkdir -Force -Verbose builds;
|
||||
cd builds;
|
||||
mkdir -Force -Verbose x86;
|
||||
mkdir -Force -Verbose x64;
|
||||
|
||||
@ -17,6 +17,7 @@ foreach ($language in @{
|
||||
THAI="Thai";
|
||||
KOREAN="Korean";
|
||||
ITALIAN="Italian";
|
||||
FRENCH="French"
|
||||
}.GetEnumerator())
|
||||
{
|
||||
$folder = "Textractor-$($language.Value)-$version";
|
||||
@ -34,30 +35,39 @@ foreach ($language in @{
|
||||
foreach ($file in @(
|
||||
"Textractor.exe",
|
||||
"TextractorCLI.exe",
|
||||
"texthook.dll",
|
||||
"Bing Translate.dll",
|
||||
"Copy to Clipboard.dll",
|
||||
"DeepL Translate.dll",
|
||||
"Extra Newlines.dll",
|
||||
"Extra Window.dll",
|
||||
"Google Translate.dll",
|
||||
"Lua.dll",
|
||||
"Regex Filter.dll",
|
||||
"Remove Repeated Characters.dll",
|
||||
"Remove Repeated Phrases.dll",
|
||||
"Remove Repeated Phrases 2.dll",
|
||||
"Remove 30 Repeated Sentences.dll",
|
||||
"Replacer.dll",
|
||||
"Thread Linker.dll"
|
||||
"texthook.dll"
|
||||
))
|
||||
{
|
||||
copy -Force -Recurse -Verbose -Destination "$folder/$arch" -Path "Release_$arch/$file";
|
||||
}
|
||||
foreach ($extension in @(
|
||||
"Bing Translate",
|
||||
"Copy to Clipboard",
|
||||
"DeepL Translate",
|
||||
"DevTools DeepL Translate",
|
||||
"DevTools Papago Translate",
|
||||
"DevTools Systran Translate",
|
||||
"Extra Newlines",
|
||||
"Extra Window",
|
||||
"Google Translate",
|
||||
"Lua",
|
||||
"Regex Filter",
|
||||
"Regex Replacer",
|
||||
"Remove Repeated Characters",
|
||||
"Remove Repeated Phrases",
|
||||
"Remove Repeated Phrases 2",
|
||||
"Remove 30 Repeated Sentences",
|
||||
"Replacer",
|
||||
"Styler",
|
||||
"Thread Linker"
|
||||
))
|
||||
{
|
||||
copy -Force -Recurse -Verbose -Destination "$folder/$arch/$extension.xdll" -Path "Release_$arch/$extension.dll";
|
||||
}
|
||||
}
|
||||
&"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /v /t "http://timestamp.digicert.com" /fd SHA256 @(dir "$folder\**\*");
|
||||
}
|
||||
|
||||
&"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /v /t "http://timestamp.digicert.com" /fd SHA256 @(dir "Textractor-*-$version\**\*");
|
||||
|
||||
rm -Force -Recurse -Verbose "Runtime";
|
||||
mkdir -Force -Verbose "Runtime";
|
||||
foreach ($arch in @("x86", "x64"))
|
||||
@ -68,6 +78,9 @@ foreach ($arch in @("x86", "x64"))
|
||||
"LocaleEmulator.dll",
|
||||
"Qt5Core.dll",
|
||||
"Qt5Gui.dll",
|
||||
"Qt5Network.dll",
|
||||
"Qt5WebSockets.dll",
|
||||
"Qt5WinExtras.dll"
|
||||
"Qt5Widgets.dll",
|
||||
"platforms",
|
||||
"styles"
|
||||
@ -75,12 +88,13 @@ foreach ($arch in @("x86", "x64"))
|
||||
{
|
||||
copy -Force -Recurse -Verbose -Destination "Runtime/$arch/$file" -Path "Release_$arch/$file";
|
||||
}
|
||||
copy -Force -Recurse -Verbose -Destination "Runtime/$arch" -Path "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Redist/MSVC/**/$arch/Microsoft.VC142.CRT/*"
|
||||
}
|
||||
|
||||
rm -Force -Recurse -Verbose "Textractor";
|
||||
mkdir -Force -Verbose "Textractor";
|
||||
copy -Force -Recurse -Verbose -Destination "Textractor" -Path @("Runtime/*", "Textractor--$version/*");
|
||||
&"C:\Program Files\7-Zip\7z" a "Textractor-$version-Zip-Version-English-Only.zip" Textractor/
|
||||
&"C:\Program Files\7-Zip\7z" a "Textractor-$version-Zip-Version-English-Only.zip" Textractor/ ../INSTALL_THIS_UNICODE_FONT.ttf
|
||||
|
||||
cd ..;
|
||||
&"C:\Program Files (x86)\Inno Setup 6\iscc" -DVERSION="$version" installer.iss;
|
||||
|
32
docs/CREDITS.md
Normal file
32
docs/CREDITS.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Developers
|
||||
|
||||
- Textractor mainly made by [Artikash](https://github.com/Artikash) with the contributions of
|
||||
- Build system improvements by [DoumanAsh](https://github.com/DoumanAsh)
|
||||
- Original text removal in Extra Window by [Niakr1s](https://github.com/Niakr1s)
|
||||
- CLI bugfixes by [tinyAdapter](https://github.com/tinyAdapter)
|
||||
- Hook codes for WAFFLE and WillPlus by [lgztx96](https://github.com/lgztx96)
|
||||
- Hook codes for TokyoNecro by [Jazzinghen](https://github.com/Jazzinghen)
|
||||
- Hook codes for ShinyDays and Artemis by [luojunyuan](https://github.com/luojunyuan)
|
||||
- Google Chrome DevTools integration by [zeheyler](https://github.com/zeheyler)
|
||||
- AppVeyor setup by [silas1037](https://github.com/silas1037)
|
||||
- Improved attach dialog by [tera8m4](https://github.com/tera8m4)
|
||||
- Regex Replacer, Papago, Systran, improvements to Extra Window, and bugfixes by [Blu3train](https://github.com/Blu3train)
|
||||
- French translation by [Racky](mailto:maitrenoah@gmail.com) and [Gratusfr](https://github.com/Gratusfr)
|
||||
- Spanish translation by [scese250](https://github.com/scese250)
|
||||
- Turkish translation by [niisokusu](https://reddit.com/u/niisokusu)
|
||||
- Simplified Chinese translation by [tinyAdapter](https://github.com/tinyAdapter), [lgztx96](https://github.com/lgztx96) and [chinanoahli](https://github.com/chinanoahli)
|
||||
- Russian translation by [TokcDK](https://github.com/TokcDK)
|
||||
- Indonesian translation by [Hawxone](https://github.com/Hawxone)
|
||||
- Portuguese translation by [TsumiHokiro](https://github.com/TsumiHokiro)
|
||||
- Thai translation by [azmadoppler](https://github.com/azmadoppler)
|
||||
- Korean translation by [O SK](mailto:afkl11@outlook.kr)
|
||||
- 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](mailto:zorkzero@hotmail.com)
|
||||
- VNR engine made by [jichi](https://github.com/jichifly)
|
||||
- ITH updated by [Andys](https://github.com/AndyScull)
|
||||
- ITH originally made by [kaosu](https://code.google.com/archive/p/interactive-text-hooker)
|
||||
|
||||
If you're on this list and want your link changed let Artikash know.
|
||||
|
||||
# THANK YOU!!
|
7
docs/TUTORIAL.md
Normal file
7
docs/TUTORIAL.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Tutorial Video
|
||||
|
||||
https://www.youtube.com/watch?v=eecEOacF6mw
|
||||
|
||||
## Updates/Corrections
|
||||
|
||||
Automatically finding hooks is now done via the `Search for hooks` button, the method shown in the video is found in the `Search for specific text` option.
|
@ -1,28 +1,64 @@
|
||||
include(QtUtils)
|
||||
msvc_registry_search()
|
||||
find_qt5(Core Widgets)
|
||||
|
||||
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(DevTools\ Papago\ Translate MODULE devtoolspapagotranslate.cpp devtools.cpp translatewrapper.cpp network.cpp extensionimpl.cpp)
|
||||
add_library(DevTools\ Systran\ Translate MODULE devtoolssystrantranslate.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)
|
||||
add_library(Lua MODULE lua.cpp extensionimpl.cpp)
|
||||
add_library(Regex\ Filter MODULE regexfilter.cpp extensionimpl.cpp)
|
||||
add_library(Regex\ Replacer MODULE regexreplacer.cpp extensionimpl.cpp)
|
||||
add_library(Remove\ Repeated\ Characters MODULE removerepeatchar.cpp extensionimpl.cpp)
|
||||
add_library(Remove\ Repeated\ Phrases MODULE removerepeatphrase.cpp extensionimpl.cpp)
|
||||
add_library(Remove\ Repeated\ Phrases\ 2 MODULE removerepeatphrase2.cpp extensionimpl.cpp)
|
||||
add_library(Remove\ 30\ Repeated\ Sentences MODULE removerepeatsentence.cpp extensionimpl.cpp)
|
||||
add_library(Replacer MODULE replacer.cpp extensionimpl.cpp)
|
||||
add_library(Styler MODULE styler.cpp extensionimpl.cpp)
|
||||
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(DevTools\ Papago\ Translate REUSE_FROM pch)
|
||||
target_precompile_headers(DevTools\ Systran\ 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)
|
||||
target_precompile_headers(Lua REUSE_FROM pch)
|
||||
target_precompile_headers(Regex\ Filter REUSE_FROM pch)
|
||||
target_precompile_headers(Regex\ Replacer REUSE_FROM pch)
|
||||
target_precompile_headers(Remove\ Repeated\ Characters REUSE_FROM pch)
|
||||
target_precompile_headers(Remove\ Repeated\ Phrases REUSE_FROM pch)
|
||||
target_precompile_headers(Remove\ Repeated\ Phrases\ 2 REUSE_FROM pch)
|
||||
target_precompile_headers(Remove\ 30\ Repeated\ Sentences REUSE_FROM pch)
|
||||
target_precompile_headers(Replacer REUSE_FROM pch)
|
||||
target_precompile_headers(Styler REUSE_FROM pch)
|
||||
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(DevTools\ Papago\ Translate shell32 winhttp Qt5::Widgets Qt5::WebSockets)
|
||||
target_link_libraries(DevTools\ Systran\ 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(Styler Qt5::Widgets)
|
||||
target_link_libraries(Thread\ Linker Qt5::Widgets)
|
||||
|
||||
add_custom_target(Cleaner ALL COMMAND del *.xdll WORKING_DIRECTORY ${CMAKE_FINAL_OUTPUT_DIRECTORY})
|
||||
|
||||
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()
|
||||
|
@ -1,114 +1,241 @@
|
||||
#include "extension.h"
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
#include <QStringList>
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
extern Synchronized<std::wstring> translateTo, apiKey;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "Bing Translate";
|
||||
const char* GET_API_KEY_FROM = "https://www.microsoft.com/en-us/translator/business/trial/#get-started";
|
||||
QStringList languages
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Afrikaans: af",
|
||||
"Arabic: ar",
|
||||
"Bangla: bn",
|
||||
"Bosnian: bs",
|
||||
"Bulgarian: bg",
|
||||
"Cantonese (traditional): yue",
|
||||
"Catalan: ca",
|
||||
"Chinese (simplified): zh-Hans",
|
||||
"Chinese (traditional): zh-Hant",
|
||||
"Croatian: hr",
|
||||
"Czech: cs",
|
||||
"Danish: da",
|
||||
"Dutch: nl",
|
||||
"English: en",
|
||||
"Estonian: et",
|
||||
"Fijian: fj",
|
||||
"Filipino: fil",
|
||||
"Finnish: fi",
|
||||
"French: fr",
|
||||
"German: de",
|
||||
"Greek: el",
|
||||
"Haitian Creole: ht",
|
||||
"Hebrew: he",
|
||||
"Hindi: hi",
|
||||
"Hmong Daw: mww",
|
||||
"Hungarian: hu",
|
||||
"Icelandic: is",
|
||||
"Indonesian: id",
|
||||
"Irish: ga",
|
||||
"Italian: it",
|
||||
"Japanese: ja",
|
||||
"Kannada: kn",
|
||||
"Klingon: tlh",
|
||||
"Korean: ko",
|
||||
"Latvian: lv",
|
||||
"Lithuanian: lt",
|
||||
"Malagasy: mg",
|
||||
"Malay: ms",
|
||||
"Malayalam: ml",
|
||||
"Maltese: mt",
|
||||
"Maori: mi",
|
||||
"Norwegian: nb",
|
||||
"Persian: fa",
|
||||
"Polish: pl",
|
||||
"Portuguese (Brazil): pt",
|
||||
"Portuguese (Portugal): pt-pt",
|
||||
"Punjabi: pa",
|
||||
"Romanian: ro",
|
||||
"Russian: ru",
|
||||
"Samoan: sm",
|
||||
"Serbian (Cyrillic): sr-Cyrl",
|
||||
"Serbian (Latin): sr-Latn",
|
||||
"Slovak: sk",
|
||||
"Slovenian: sl",
|
||||
"Spanish: es",
|
||||
"Swahili: sw",
|
||||
"Swedish: sv",
|
||||
"Tahitian: ty",
|
||||
"Tamil: ta",
|
||||
"Telugu: te",
|
||||
"Thai: th",
|
||||
"Tongan: to",
|
||||
"Turkish: tr",
|
||||
"Ukrainian: uk",
|
||||
"Urdu: ur",
|
||||
"Vietnamese: vi",
|
||||
"Welsh: cy",
|
||||
"Yucatec Maya: yua"
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Assamese",
|
||||
"Azerbaijani",
|
||||
"Bangla",
|
||||
"Bosnian (Latin)",
|
||||
"Bulgarian",
|
||||
"Cantonese (Traditional)",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dari",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Fijian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"French (Canada)",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong Daw",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Indonesian",
|
||||
"Inuktitut",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Klingon",
|
||||
"Korean",
|
||||
"Kurdish (Central)",
|
||||
"Kurdish (Northern)",
|
||||
"Lao",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Myanmar",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese (Brazil)",
|
||||
"Portuguese (Portugal)",
|
||||
"Punjabi",
|
||||
"Queretaro Otomi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Serbian (Cyrillic)",
|
||||
"Serbian (Latin)",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tahitian",
|
||||
"Tamil",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Tigrinya",
|
||||
"Tongan",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Yucatec Maya"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Assamese" }, { L"as" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Bangla" }, { L"bn" } },
|
||||
{ { L"Bosnian (Latin)" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Cantonese (Traditional)" }, { L"yue" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-Hans" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-Hant" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dari" }, { L"prs" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Fijian" }, { L"fj" } },
|
||||
{ { L"Filipino" }, { L"fil" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"French (Canada)" }, { L"fr-ca" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong Daw" }, { L"mww" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Inuktitut" }, { L"iu" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Klingon" }, { L"tlh-Latn" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Central)" }, { L"ku" } },
|
||||
{ { L"Kurdish (Northern)" }, { L"kmr" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Myanmar" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"nb" } },
|
||||
{ { L"Odia" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese (Brazil)" }, { L"pt" } },
|
||||
{ { L"Portuguese (Portugal)" }, { L"pt-pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Queretaro Otomi" }, { L"otq" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Serbian (Cyrillic)" }, { L"sr-Cyrl" } },
|
||||
{ { L"Serbian (Latin)" }, { L"sr-Latn" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tahitian" }, { L"ty" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Tigrinya" }, { L"ti" } },
|
||||
{ { L"Tongan" }, { L"to" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Yucatec Maya" }, { L"yua" } },
|
||||
{ { L"?" }, { L"auto-detect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
|
||||
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 500;
|
||||
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, SentenceInfo)
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!apiKey->empty())
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&from=" + codes.at(tlp.translateFrom);
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
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(L"Content-Type: application/json; charset=UTF-8\r\nOcp-Apim-Subscription-Key:%s", apiKey.Copy()).c_str()
|
||||
FormatString(L"/translate?api-version=3.0&to=%s%s", codes.at(tlp.translateTo), translateFromComponent).c_str(),
|
||||
FormatString(R"([{"text":"%s"}])", JSON::Escape(WideStringToString(text))),
|
||||
FormatString(L"Content-Type: application/json; charset=UTF-8\r\nOcp-Apim-Subscription-Key:%s", tlp.authKey).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) };
|
||||
}
|
||||
|
||||
static std::atomic<int> i = 0;
|
||||
static Synchronized<std::wstring> token;
|
||||
if (token->empty()) if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"www.bing.com", L"GET", L"translator" })
|
||||
{
|
||||
std::wstring tokenBuilder;
|
||||
if (auto tokenPos = httpRequest.response.find(L"[" + std::to_wstring(time(nullptr) / 100)); tokenPos != std::string::npos)
|
||||
tokenBuilder = FormatString(L"&key=%s&token=%s", httpRequest.response.substr(tokenPos + 1, 13), httpRequest.response.substr(tokenPos + 16, 32));
|
||||
if (auto tokenPos = httpRequest.response.find(L"IG:\""); tokenPos != std::string::npos)
|
||||
tokenBuilder += L"&IG=" + httpRequest.response.substr(tokenPos + 4, 32);
|
||||
if (auto tokenPos = httpRequest.response.find(L"data-iid=\""); tokenPos != std::string::npos)
|
||||
tokenBuilder += L"&IID=" + httpRequest.response.substr(tokenPos + 10, 15);
|
||||
if (!tokenBuilder.empty()) token->assign(tokenBuilder);
|
||||
else return { false, FormatString(L"%s: %s\ntoken not found", TRANSLATION_ERROR, httpRequest.response) };
|
||||
}
|
||||
else return { false, FormatString(L"%s: could not acquire token", TRANSLATION_ERROR) };
|
||||
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"www.bing.com",
|
||||
L"POST",
|
||||
FormatString(L"/ttranslatev3?fromLang=auto-detect&to=%s&text=%s", translateTo.Copy(), Escape(text)).c_str()
|
||||
FormatString(L"/ttranslatev3?fromLang=%s&to=%s&text=%s%s.%d", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text), token.Copy(), i++).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] };
|
||||
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
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 (token=%s): %s", TRANSLATION_ERROR, std::exchange(token.Acquire().contents, L""), httpRequest.response) };
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include <istream>
|
||||
|
||||
template <typename C, int delimiterCount, int blockSize = 0x1000 / sizeof(C)> // windows file block size
|
||||
class BlockMarkupIterator
|
||||
{
|
||||
public:
|
||||
BlockMarkupIterator(const std::istream& stream, const std::basic_string_view<C>(&delimiters)[delimiterCount]) :
|
||||
streambuf(*stream.rdbuf())
|
||||
BlockMarkupIterator(const std::istream& stream, const std::basic_string_view<C>(&delimiters)[delimiterCount]) : streambuf(*stream.rdbuf())
|
||||
{
|
||||
std::copy_n(delimiters, delimiterCount, this->delimiters.begin());
|
||||
}
|
||||
@ -51,7 +49,7 @@ private:
|
||||
}
|
||||
|
||||
static constexpr C endImpl[5] = { '|', 'E', 'N', 'D', '|' };
|
||||
static constexpr std::basic_string_view end{ endImpl, 5 };
|
||||
static constexpr std::basic_string_view<C> end{ endImpl, 5 };
|
||||
|
||||
std::basic_streambuf<char>& streambuf;
|
||||
std::basic_string<C> buffer;
|
||||
|
@ -1,55 +1,143 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
#include <random>
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
extern const char* USE_PREV_SENTENCE_CONTEXT;
|
||||
|
||||
extern Synchronized<std::wstring> translateTo, apiKey;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DeepL Translate";
|
||||
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html";
|
||||
QStringList languages
|
||||
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html#developer";
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Chinese (simplified): ZH",
|
||||
"Dutch: NL",
|
||||
"English: EN",
|
||||
"French: FR",
|
||||
"German: DE",
|
||||
"Italian: IT",
|
||||
"Japanese: JA",
|
||||
"Polish: PL",
|
||||
"Portuguese: PT",
|
||||
"Russian: RU",
|
||||
"Spanish: ES",
|
||||
"Bulgarian",
|
||||
"Chinese (Simplified)",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English (American)",
|
||||
"English (British)",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese (Brazil)",
|
||||
"Portuguese (Portugal)",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Turkish"
|
||||
},
|
||||
languagesFrom
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Turkish"
|
||||
};
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Bulgarian" }, { L"BG" } },
|
||||
{ { L"Chinese" }, { L"ZH" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"ZH" } },
|
||||
{ { L"Czech" }, { L"CS" } },
|
||||
{ { L"Danish" }, { L"DA" } },
|
||||
{ { L"Dutch" }, { L"NL" } },
|
||||
{ { L"English" }, { L"EN" } },
|
||||
{ { L"English (American)" }, { L"EN-US" } },
|
||||
{ { L"English (British)" }, { L"EN-GB" } },
|
||||
{ { L"Estonian" }, { L"ET" } },
|
||||
{ { L"Finnish" }, { L"FI" } },
|
||||
{ { L"French" }, { L"FR" } },
|
||||
{ { L"German" }, { L"DE" } },
|
||||
{ { L"Greek" }, { L"EL" } },
|
||||
{ { L"Hungarian" }, { L"HU" } },
|
||||
{ { L"Indonesian" }, { L"ID" } },
|
||||
{ { L"Italian" }, { L"IT" } },
|
||||
{ { L"Japanese" }, { L"JA" } },
|
||||
{ { L"Latvian" }, { L"LV" } },
|
||||
{ { L"Lithuanian" }, { L"LT" } },
|
||||
{ { L"Polish" }, { L"PL" } },
|
||||
{ { L"Portuguese" }, { L"PT" } },
|
||||
{ { L"Portuguese (Brazil)" }, { L"PT-BR" } },
|
||||
{ { L"Portuguese (Portugal)" }, { L"PT-PT" } },
|
||||
{ { L"Romanian" }, { L"RO" } },
|
||||
{ { L"Russian" }, { L"RU" } },
|
||||
{ { L"Slovak" }, { L"SK" } },
|
||||
{ { L"Slovenian" }, { L"SL" } },
|
||||
{ { L"Spanish" }, { L"ES" } },
|
||||
{ { L"Swedish" }, { L"SV" } },
|
||||
{ { L"Turkish" }, { L"TR" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, rateLimitAll = true, rateLimitSelected = true, useCache = false;
|
||||
int tokenCount = 10, tokenRestoreDelay = 60000, maxSentenceSize = 500;
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = true, useCache = true, useFilter = true;
|
||||
int tokenCount = 10, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
const wchar_t* accept[] = { L"*/*", nullptr };
|
||||
Synchronized<std::wstring> LMTBID;
|
||||
enum KeyType { CAT, REST };
|
||||
int keyType = REST;
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, SentenceInfo sentenceInfo)
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
if (!apiKey->empty())
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
std::string translateFromComponent = tlp.translateFrom == L"?" ? "" : "&source_lang=" + WideStringToString(codes.at(tlp.translateFrom));
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
L"api.deepl.com",
|
||||
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
|
||||
L"POST",
|
||||
L"/v2/translate",
|
||||
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), apiKey.Copy(), translateTo.Copy()),
|
||||
keyType == CAT ? L"/v1/translate" : L"/v2/translate",
|
||||
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
|
||||
L"Content-Type: application/x-www-form-urlencoded"
|
||||
})
|
||||
}; httpRequest && (httpRequest.response.find(L"translations") != std::string::npos || (httpRequest = HttpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
|
||||
L"POST",
|
||||
(keyType = !keyType) == CAT ? L"/v1/translate" : L"/v2/translate",
|
||||
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
|
||||
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
|
||||
int id = 10000 * std::uniform_int_distribution(0, 9999)(std::random_device()) + 1;
|
||||
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()()));
|
||||
// user_preferred_langs? what should priority be? does timestamp do anything? other translation quality options?
|
||||
auto body = FormatString(R"(
|
||||
{
|
||||
@ -60,8 +148,8 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text, SentenceInfo s
|
||||
"priority": -1,
|
||||
"timestamp": %lld,
|
||||
"lang": {
|
||||
"target_lang": "%S",
|
||||
"source_lang_user_selected": "auto"
|
||||
"target_lang": "%.2S",
|
||||
"source_lang_user_selected": "%S"
|
||||
},
|
||||
"jobs": [{
|
||||
"raw_en_sentence": "%s",
|
||||
@ -73,25 +161,20 @@ std::pair<bool, std::wstring> Translate(const std::wstring& text, SentenceInfo s
|
||||
}]
|
||||
}
|
||||
}
|
||||
)", ++id, r + (n - r % n), translateTo.Copy(), JSON::Escape(text));
|
||||
)", id, r + (n - r % n), codes.at(tlp.translateTo), codes.at(tlp.translateFrom), 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" + LMTBID.Acquire().contents;
|
||||
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",
|
||||
INTERNET_DEFAULT_PORT,
|
||||
L"https://www.deepl.com/translator",
|
||||
WINHTTP_FLAG_SECURE,
|
||||
NULL,
|
||||
accept
|
||||
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) };
|
||||
}
|
||||
|
174
extensions/devtools.cpp
Normal file
174
extensions/devtools.cpp
Normal file
@ -0,0 +1,174 @@
|
||||
#include "devtools.h"
|
||||
#include "module.h"
|
||||
#include <ppltasks.h>
|
||||
#include <ShlObj.h>
|
||||
#include <QWebSocket>
|
||||
#include <QMetaEnum>
|
||||
#include <QFileDialog>
|
||||
#include <QMouseEvent>
|
||||
|
||||
extern const char* CHROME_LOCATION;
|
||||
extern const char* START_DEVTOOLS;
|
||||
extern const char* STOP_DEVTOOLS;
|
||||
extern const char* HIDE_CHROME;
|
||||
extern const char* DEVTOOLS_STATUS;
|
||||
extern const char* AUTO_START;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
|
||||
extern QFormLayout* display;
|
||||
extern Settings settings;
|
||||
|
||||
namespace
|
||||
{
|
||||
QLabel* statusLabel;
|
||||
AutoHandle<> process = NULL;
|
||||
QWebSocket webSocket;
|
||||
std::atomic<int> idCounter = 0;
|
||||
Synchronized<std::unordered_map<int, concurrency::task_completion_event<JSON::Value<wchar_t>>>> mapQueue;
|
||||
|
||||
void StatusChanged(QString status)
|
||||
{
|
||||
QMetaObject::invokeMethod(statusLabel, std::bind(&QLabel::setText, statusLabel, status));
|
||||
}
|
||||
void Start(std::wstring chromePath, bool headless)
|
||||
{
|
||||
if (process) DevTools::Close();
|
||||
|
||||
auto args = FormatString(
|
||||
L"%s --proxy-server=direct:// --disable-extensions --disable-gpu --no-first-run --user-data-dir=\"%s\\devtoolscache\" --remote-debugging-port=9222",
|
||||
chromePath,
|
||||
std::filesystem::current_path().wstring()
|
||||
);
|
||||
args += headless ? L" --window-size=1920,1080 --headless" : L" --window-size=850,900";
|
||||
DWORD exitCode = 0;
|
||||
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
|
||||
PROCESS_INFORMATION processInfo = {};
|
||||
if (!CreateProcessW(NULL, args.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &DUMMY, &processInfo)) return StatusChanged("StartupFailed");
|
||||
CloseHandle(processInfo.hThread);
|
||||
process = processInfo.hProcess;
|
||||
|
||||
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()) return webSocket.open(S(*(*it)[L"webSocketDebuggerUrl"].String()));
|
||||
|
||||
StatusChanged("ConnectingFailed");
|
||||
}
|
||||
|
||||
auto _ = ([]
|
||||
{
|
||||
QObject::connect(&webSocket, &QWebSocket::stateChanged,
|
||||
[](QAbstractSocket::SocketState state) { StatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); });
|
||||
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
|
||||
{
|
||||
auto result = JSON::Parse(S(message));
|
||||
auto mapQueue = ::mapQueue.Acquire();
|
||||
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 Initialize()
|
||||
{
|
||||
QString chromePath = settings.value(CHROME_LOCATION).toString();
|
||||
if (chromePath.isEmpty())
|
||||
{
|
||||
for (auto [_, process] : GetAllProcesses())
|
||||
if (process && (process->find(L"\\chrome.exe") != std::string::npos || process->find(L"\\msedge.exe") != std::string::npos)) chromePath = S(process.value());
|
||||
wchar_t programFiles[MAX_PATH + 100] = {};
|
||||
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);
|
||||
static struct : QObject
|
||||
{
|
||||
bool eventFilter(QObject* object, QEvent* event)
|
||||
{
|
||||
if (auto mouseEvent = dynamic_cast<QMouseEvent*>(event))
|
||||
if (mouseEvent->button() == Qt::LeftButton)
|
||||
if (QString chromePath = QFileDialog::getOpenFileName(nullptr, TRANSLATION_PROVIDER, "/", "Google Chrome (*.exe)"); !chromePath.isEmpty())
|
||||
((QLineEdit*)object)->setText(chromePath);
|
||||
return false;
|
||||
}
|
||||
} chromeSelector;
|
||||
chromePathEdit->installEventFilter(&chromeSelector);
|
||||
QObject::connect(chromePathEdit, &QLineEdit::textChanged, [chromePathEdit](QString path) { settings.setValue(CHROME_LOCATION, path); });
|
||||
display->addRow(CHROME_LOCATION, chromePathEdit);
|
||||
auto headlessCheck = new QCheckBox();
|
||||
auto startButton = new QPushButton(START_DEVTOOLS), stopButton = new QPushButton(STOP_DEVTOOLS);
|
||||
headlessCheck->setChecked(settings.value(HIDE_CHROME, true).toBool());
|
||||
QObject::connect(headlessCheck, &QCheckBox::clicked, [](bool headless) { settings.setValue(HIDE_CHROME, headless); });
|
||||
QObject::connect(startButton, &QPushButton::clicked, [chromePathEdit, headlessCheck] { Start(S(chromePathEdit->text()), headlessCheck->isChecked()); });
|
||||
QObject::connect(stopButton, &QPushButton::clicked, &Close);
|
||||
auto buttons = new QHBoxLayout();
|
||||
buttons->addWidget(startButton);
|
||||
buttons->addWidget(stopButton);
|
||||
display->addRow(HIDE_CHROME, headlessCheck);
|
||||
auto autoStartCheck = new QCheckBox();
|
||||
autoStartCheck->setChecked(settings.value(AUTO_START, false).toBool());
|
||||
QObject::connect(autoStartCheck, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
|
||||
display->addRow(AUTO_START, autoStartCheck);
|
||||
display->addRow(buttons);
|
||||
statusLabel = new QLabel("Stopped");
|
||||
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
display->addRow(DEVTOOLS_STATUS, statusLabel);
|
||||
if (autoStartCheck->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
webSocket.close();
|
||||
for (const auto& [_, task] : mapQueue.Acquire().contents) task.set_exception(std::runtime_error("closed"));
|
||||
mapQueue->clear();
|
||||
|
||||
if (process)
|
||||
{
|
||||
TerminateProcess(process, 0);
|
||||
WaitForSingleObject(process, 1000);
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
try { std::filesystem::remove_all(L"devtoolscache"); break; }
|
||||
catch (std::filesystem::filesystem_error) { continue; }
|
||||
}
|
||||
process = NULL;
|
||||
StatusChanged("Stopped");
|
||||
}
|
||||
|
||||
bool Connected()
|
||||
{
|
||||
return webSocket.state() == QAbstractSocket::ConnectedState;
|
||||
}
|
||||
|
||||
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params)
|
||||
{
|
||||
concurrency::task_completion_event<JSON::Value<wchar_t>> response;
|
||||
int id = idCounter += 1;
|
||||
if (!Connected()) return {};
|
||||
mapQueue->try_emplace(id, response);
|
||||
QMetaObject::invokeMethod(&webSocket, std::bind(&QWebSocket::sendTextMessage, &webSocket, S(FormatString(LR"({"id":%d,"method":"%S","params":%s})", id, method, params))));
|
||||
try { if (auto result = create_task(response).get()[L"result"]) return result; } catch (...) {}
|
||||
return {};
|
||||
}
|
||||
}
|
10
extensions/devtools.h
Normal file
10
extensions/devtools.h
Normal file
@ -0,0 +1,10 @@
|
||||
#include "qtcommon.h"
|
||||
#include "network.h"
|
||||
|
||||
namespace DevTools
|
||||
{
|
||||
void Initialize();
|
||||
void Close();
|
||||
bool Connected();
|
||||
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params = L"{}");
|
||||
}
|
148
extensions/devtoolsdeepltranslate.cpp
Normal file
148
extensions/devtoolsdeepltranslate.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese (Simplified)",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English (American)",
|
||||
"English (British)",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Portuguese (Brazilian)",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
},
|
||||
languagesFrom =
|
||||
{
|
||||
"Bulgarian",
|
||||
"Chinese",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Spanish",
|
||||
"Swedish"
|
||||
};
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Bulgarian" }, { L"Bulgarian" } },
|
||||
{ { L"Chinese" }, { L"Chinese" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"Chinese (simplified)" } },
|
||||
{ { L"Czech" }, { L"Czech" } },
|
||||
{ { L"Danish" }, { L"Danish" } },
|
||||
{ { L"Dutch" }, { L"Dutch" } },
|
||||
{ { L"English" }, { L"English" } },
|
||||
{ { L"English (American)" }, { L"English (American)" } },
|
||||
{ { L"English (British)" }, { L"English (British)" } },
|
||||
{ { L"Estonian" }, { L"Estonian" } },
|
||||
{ { L"Finnish" }, { L"Finnish" } },
|
||||
{ { L"French" }, { L"French" } },
|
||||
{ { L"German" }, { L"German" } },
|
||||
{ { L"Greek" }, { L"Greek" } },
|
||||
{ { L"Hungarian" }, { L"Hungarian" } },
|
||||
{ { L"Italian" }, { L"Italian" } },
|
||||
{ { L"Japanese" }, { L"Japanese" } },
|
||||
{ { L"Latvian" }, { L"Latvian" } },
|
||||
{ { L"Lithuanian" }, { L"Lithuanian" } },
|
||||
{ { L"Polish" }, { L"Polish" } },
|
||||
{ { L"Portuguese" }, { L"Portuguese" } },
|
||||
{ { L"Portuguese (Brazilian)" }, { L"Portuguese (Brazilian)" } },
|
||||
{ { L"Romanian" }, { L"Romanian" } },
|
||||
{ { L"Russian" }, { L"Russian" } },
|
||||
{ { L"Slovak" }, { L"Slovak" } },
|
||||
{ { L"Slovenian" }, { L"Slovenian" } },
|
||||
{ { L"Spanish" }, { L"Spanish" } },
|
||||
{ { L"Swedish" }, { L"Swedish" } },
|
||||
{ { L"?" }, { L"Detect language" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
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);
|
||||
std::wstring escaped; // DeepL breaks with slash in input
|
||||
for (auto ch : text) ch == '/' ? escaped += L"\\/" : escaped += ch;
|
||||
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/en/translator#en/en/%s"})", Escape(escaped)));
|
||||
for (int retry = 0; ++retry < 20; Sleep(100))
|
||||
if (Copy(DevTools::SendRequest("Runtime.evaluate", LR"({"expression":"document.readyState"})")[L"result"][L"value"].String()) == L"complete") break;
|
||||
|
||||
DevTools::SendRequest("Runtime.evaluate", FormatString(LR"({"expression":"
|
||||
document.querySelector('.lmt__language_select--source').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
document.querySelector('.lmt__language_select--target').querySelector('button').click();
|
||||
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
|
||||
"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo)));
|
||||
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
if (auto errorMessage = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('div.lmt__system_notification').innerHTML","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, errorMessage.value()) };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
82
extensions/devtoolspapagotranslate.cpp
Normal file
82
extensions/devtoolspapagotranslate.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Papago Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"English",
|
||||
"French",
|
||||
"German",
|
||||
"Hindi",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Spanish",
|
||||
"Thai",
|
||||
"Vietnamese",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt-TW" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
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("Page.navigate", FormatString(LR"({"url":"https://papago.naver.com/?sk=%s&tk=%s&st=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)));
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#txtTarget').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
152
extensions/devtoolssystrantranslate.cpp
Normal file
152
extensions/devtoolssystrantranslate.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "qtcommon.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "devtools.h"
|
||||
|
||||
extern const wchar_t* ERROR_START_CHROME;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "DevTools Systran Translate";
|
||||
const char* GET_API_KEY_FROM = nullptr;
|
||||
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Albanian",
|
||||
"Arabic",
|
||||
"Bengali",
|
||||
"Bulgarian",
|
||||
"Burmese",
|
||||
"Catalan",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Estonian",
|
||||
"Finnish",
|
||||
"French",
|
||||
"German",
|
||||
"Greek",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hungarian",
|
||||
"Indonesian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Malay",
|
||||
"Norwegian",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Serbian",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Swedish",
|
||||
"Tagalog",
|
||||
"Tamil",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Vietnamese"
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Burmese" }, { L"my" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zt" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Hebrew" }, { L"he" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tagalog" }, { L"tl" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"?" }, { L"autodetect" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
DevTools::Initialize();
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
DevTools::Close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
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(
|
||||
"Page.navigate",
|
||||
FormatString(LR"({"url":"https://translate.systran.net/?source=%s&target=%s&input=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text))
|
||||
);
|
||||
for (int retry = 0; ++retry < 100; Sleep(100))
|
||||
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
|
||||
LR"({"expression":"document.querySelector('#outputEditor').textContent.trim() ","returnByValue":true})"
|
||||
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
|
||||
return { false, TRANSLATION_ERROR };
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
struct InfoForExtension
|
||||
{
|
||||
const char* name;
|
||||
|
@ -17,12 +17,12 @@ extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const
|
||||
{
|
||||
try
|
||||
{
|
||||
std::wstring sentenceStr(sentence);
|
||||
int origLength = sentenceStr.size();
|
||||
if (ProcessSentence(sentenceStr, SentenceInfo{ sentenceInfo }))
|
||||
std::wstring sentenceCopy(sentence);
|
||||
int oldSize = sentenceCopy.size();
|
||||
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
|
||||
{
|
||||
if (sentenceStr.size() > origLength) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceStr.size() + 1) * sizeof(wchar_t));
|
||||
wcscpy_s(sentence, sentenceStr.size() + 1, sentenceStr.c_str());
|
||||
if (sentenceCopy.size() > oldSize) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceCopy.size() + 1) * sizeof(wchar_t));
|
||||
wcscpy_s(sentence, sentenceCopy.size() + 1, sentenceCopy.c_str());
|
||||
}
|
||||
}
|
||||
catch (SKIP)
|
||||
|
@ -13,15 +13,20 @@
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QAbstractNativeEventFilter>
|
||||
|
||||
extern const char* EXTRA_WINDOW_INFO;
|
||||
extern const char* SENTENCE_TOO_BIG;
|
||||
extern const char* MAX_SENTENCE_SIZE;
|
||||
extern const char* TOPMOST;
|
||||
extern const char* OPACITY;
|
||||
extern const char* SHOW_ORIGINAL;
|
||||
extern const char* SHOW_ORIGINAL_INFO;
|
||||
extern const char* ORIGINAL_AFTER_TRANSLATION;
|
||||
extern const char* SIZE_LOCK;
|
||||
extern const char* POSITION_LOCK;
|
||||
extern const char* CENTERED_TEXT;
|
||||
extern const char* AUTO_RESIZE_WINDOW_HEIGHT;
|
||||
extern const char* CLICK_THROUGH;
|
||||
extern const char* HIDE_MOUSEOVER;
|
||||
extern const char* DICTIONARY;
|
||||
extern const char* DICTIONARY_INSTRUCTIONS;
|
||||
extern const char* BG_COLOR;
|
||||
@ -31,9 +36,9 @@ extern const char* OUTLINE_COLOR;
|
||||
extern const char* OUTLINE_SIZE;
|
||||
extern const char* OUTLINE_SIZE_INFO;
|
||||
extern const char* FONT;
|
||||
extern const char* SAVE_SETTINGS;
|
||||
|
||||
constexpr auto DICTIONARY_SAVE_FILE = u8"SavedDictionary.txt";
|
||||
constexpr int CLICK_THROUGH_HOTKEY = 0xc0d0;
|
||||
|
||||
QColor colorPrompt(QWidget* parent, QColor default, const QString& title, bool customOpacity = true)
|
||||
{
|
||||
@ -42,29 +47,35 @@ QColor colorPrompt(QWidget* parent, QColor default, const QString& title, bool c
|
||||
return color;
|
||||
}
|
||||
|
||||
struct PrettyWindow : QDialog
|
||||
struct PrettyWindow : QDialog, Localizer
|
||||
{
|
||||
PrettyWindow(const char* name)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
ui.display->setGraphicsEffect(&outliner);
|
||||
ui.display->setGraphicsEffect(outliner = new Outliner);
|
||||
setWindowFlags(Qt::FramelessWindowHint);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
settings.beginGroup(name);
|
||||
QFont font = ui.display->font();
|
||||
if (font.fromString(settings.value(FONT, font.toString()).toString())) ui.display->setFont(font);
|
||||
setBgColor(settings.value(BG_COLOR, bgColor).value<QColor>());
|
||||
setTextColor(settings.value(TEXT_COLOR, textColor()).value<QColor>());
|
||||
outliner.color = settings.value(OUTLINE_COLOR, outliner.color).value<QColor>();
|
||||
outliner.size = settings.value(OUTLINE_SIZE, outliner.size).toDouble();
|
||||
SetBackgroundColor(settings.value(BG_COLOR, backgroundColor).value<QColor>());
|
||||
SetTextColor(settings.value(TEXT_COLOR, TextColor()).value<QColor>());
|
||||
outliner->color = settings.value(OUTLINE_COLOR, outliner->color).value<QColor>();
|
||||
outliner->size = settings.value(OUTLINE_SIZE, outliner->size).toDouble();
|
||||
autoHide = settings.value(HIDE_MOUSEOVER, autoHide).toBool();
|
||||
menu.addAction(FONT, this, &PrettyWindow::RequestFont);
|
||||
menu.addAction(BG_COLOR, [this] { setBgColor(colorPrompt(this, bgColor, BG_COLOR)); });
|
||||
menu.addAction(TEXT_COLOR, [this] { setTextColor(colorPrompt(this, textColor(), TEXT_COLOR)); });
|
||||
QAction* outlineAction = menu.addAction(TEXT_OUTLINE, this, &PrettyWindow::setOutline);
|
||||
menu.addAction(BG_COLOR, [this] { SetBackgroundColor(colorPrompt(this, backgroundColor, BG_COLOR)); });
|
||||
menu.addAction(TEXT_COLOR, [this] { SetTextColor(colorPrompt(this, TextColor(), TEXT_COLOR)); });
|
||||
QAction* outlineAction = menu.addAction(TEXT_OUTLINE, this, &PrettyWindow::SetOutline);
|
||||
outlineAction->setCheckable(true);
|
||||
outlineAction->setChecked(outliner.size >= 0);
|
||||
connect(ui.display, &QLabel::customContextMenuRequested, [this](QPoint point) { menu.exec(mapToGlobal(point)); });
|
||||
outlineAction->setChecked(outliner->size >= 0);
|
||||
QAction* autoHideAction = menu.addAction(HIDE_MOUSEOVER, this, [this](bool autoHide) { settings.setValue(HIDE_MOUSEOVER, this->autoHide = autoHide); });
|
||||
autoHideAction->setCheckable(true);
|
||||
autoHideAction->setChecked(autoHide);
|
||||
connect(this, &QDialog::customContextMenuRequested, [this](QPoint point) { menu.exec(mapToGlobal(point)); });
|
||||
connect(ui.display, &QLabel::customContextMenuRequested, [this](QPoint point) { menu.exec(ui.display->mapToGlobal(point)); });
|
||||
startTimer(50);
|
||||
}
|
||||
|
||||
~PrettyWindow()
|
||||
@ -75,8 +86,33 @@ struct PrettyWindow : QDialog
|
||||
Ui::ExtraWindow ui;
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent*) override
|
||||
{
|
||||
if (autoHide && geometry().contains(QCursor::pos()))
|
||||
{
|
||||
if (!hidden)
|
||||
{
|
||||
if (backgroundColor.alphaF() > 0.05) backgroundColor.setAlphaF(0.05);
|
||||
if (outliner->color.alphaF() > 0.05) outliner->color.setAlphaF(0.05);
|
||||
QColor hiddenTextColor = TextColor();
|
||||
if (hiddenTextColor.alphaF() > 0.05) hiddenTextColor.setAlphaF(0.05);
|
||||
ui.display->setPalette(QPalette(hiddenTextColor, {}, {}, {}, {}, {}, {}));
|
||||
hidden = true;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
else if (hidden)
|
||||
{
|
||||
backgroundColor.setAlpha(settings.value(BG_COLOR).value<QColor>().alpha());
|
||||
outliner->color.setAlpha(settings.value(OUTLINE_COLOR).value<QColor>().alpha());
|
||||
ui.display->setPalette(QPalette(settings.value(TEXT_COLOR).value<QColor>(), {}, {}, {}, {}, {}, {}));
|
||||
hidden = false;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
QMenu menu{ ui.display };
|
||||
QSettings settings{ openSettings(this) };
|
||||
Settings settings{ this };
|
||||
|
||||
private:
|
||||
void RequestFont()
|
||||
@ -88,47 +124,48 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
void setBgColor(QColor color)
|
||||
void SetBackgroundColor(QColor color)
|
||||
{
|
||||
if (!color.isValid()) return;
|
||||
if (color.alpha() == 0) color.setAlpha(1);
|
||||
bgColor = color;
|
||||
backgroundColor = color;
|
||||
repaint();
|
||||
settings.setValue(BG_COLOR, color.name(QColor::HexArgb));
|
||||
};
|
||||
|
||||
QColor textColor()
|
||||
QColor TextColor()
|
||||
{
|
||||
return ui.display->palette().color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
void setTextColor(QColor color)
|
||||
void SetTextColor(QColor color)
|
||||
{
|
||||
if (!color.isValid()) return;
|
||||
ui.display->setPalette(QPalette(color, {}, {}, {}, {}, {}, {}));
|
||||
settings.setValue(TEXT_COLOR, color.name(QColor::HexArgb));
|
||||
};
|
||||
|
||||
void setOutline(bool enable)
|
||||
void SetOutline(bool enable)
|
||||
{
|
||||
if (enable)
|
||||
{
|
||||
QColor color = colorPrompt(this, outliner.color, OUTLINE_COLOR);
|
||||
if (color.isValid()) outliner.color = color;
|
||||
outliner.size = QInputDialog::getDouble(this, OUTLINE_SIZE, OUTLINE_SIZE_INFO, 0.5, 0, INT_MAX, 2, nullptr, Qt::WindowCloseButtonHint);
|
||||
QColor color = colorPrompt(this, outliner->color, OUTLINE_COLOR);
|
||||
if (color.isValid()) outliner->color = color;
|
||||
outliner->size = QInputDialog::getDouble(this, OUTLINE_SIZE, OUTLINE_SIZE_INFO, -outliner->size, 0, INT_MAX, 2, nullptr, Qt::WindowCloseButtonHint);
|
||||
}
|
||||
else outliner.size = -1;
|
||||
settings.setValue(OUTLINE_COLOR, outliner.color.name(QColor::HexArgb));
|
||||
settings.setValue(OUTLINE_SIZE, outliner.size);
|
||||
else outliner->size = -outliner->size;
|
||||
settings.setValue(OUTLINE_COLOR, outliner->color.name(QColor::HexArgb));
|
||||
settings.setValue(OUTLINE_SIZE, outliner->size);
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent*) override
|
||||
{
|
||||
QPainter(this).fillRect(rect(), bgColor);
|
||||
QPainter(this).fillRect(rect(), backgroundColor);
|
||||
}
|
||||
|
||||
QColor bgColor{ palette().window().color() };
|
||||
struct : QGraphicsEffect
|
||||
bool autoHide = false, hidden = false;
|
||||
QColor backgroundColor{ palette().window().color() };
|
||||
struct Outliner : QGraphicsEffect
|
||||
{
|
||||
void draw(QPainter* painter) override
|
||||
{
|
||||
@ -147,25 +184,27 @@ private:
|
||||
painter->drawPixmap(offset, pixmap);
|
||||
}
|
||||
QColor color{ Qt::black };
|
||||
double size = -1;
|
||||
} outliner;
|
||||
double size = -0.5;
|
||||
}* outliner;
|
||||
};
|
||||
|
||||
class ExtraWindow : public PrettyWindow
|
||||
class ExtraWindow : public PrettyWindow, QAbstractNativeEventFilter
|
||||
{
|
||||
public:
|
||||
ExtraWindow() :
|
||||
PrettyWindow("Extra Window")
|
||||
ExtraWindow() : PrettyWindow("Extra Window")
|
||||
{
|
||||
ui.display->setTextFormat(Qt::PlainText);
|
||||
if (settings.contains(WINDOW) && QGuiApplication::screenAt(settings.value(WINDOW).toRect().bottomRight())) setGeometry(settings.value(WINDOW).toRect());
|
||||
maxSentenceSize = settings.value(MAX_SENTENCE_SIZE, maxSentenceSize).toInt();
|
||||
if (settings.contains(WINDOW) && QApplication::screenAt(settings.value(WINDOW).toRect().bottomRight())) setGeometry(settings.value(WINDOW).toRect());
|
||||
|
||||
for (auto [name, default, slot] : Array<const char*, bool, void(ExtraWindow::*)(bool)>{
|
||||
{ TOPMOST, false, &ExtraWindow::setTopmost },
|
||||
{ SIZE_LOCK, false, &ExtraWindow::setLock },
|
||||
{ SHOW_ORIGINAL, true, &ExtraWindow::setShowOriginal },
|
||||
{ DICTIONARY, false, &ExtraWindow::setUseDictionary },
|
||||
{ TOPMOST, false, &ExtraWindow::SetTopmost },
|
||||
{ SIZE_LOCK, false, &ExtraWindow::SetSizeLock },
|
||||
{ POSITION_LOCK, false, &ExtraWindow::SetPositionLock },
|
||||
{ CENTERED_TEXT, false, &ExtraWindow::SetCenteredText },
|
||||
{ AUTO_RESIZE_WINDOW_HEIGHT, false, &ExtraWindow::SetAutoResize },
|
||||
{ SHOW_ORIGINAL, true, &ExtraWindow::SetShowOriginal },
|
||||
{ ORIGINAL_AFTER_TRANSLATION, true, &ExtraWindow::SetShowOriginalAfterTranslation },
|
||||
{ DICTIONARY, false, &ExtraWindow::SetUseDictionary },
|
||||
})
|
||||
{
|
||||
// delay processing anything until Textractor has finished initializing
|
||||
@ -174,15 +213,15 @@ public:
|
||||
action->setCheckable(true);
|
||||
action->setChecked(default);
|
||||
}
|
||||
menu.addAction(MAX_SENTENCE_SIZE, this, [this]
|
||||
{
|
||||
settings.setValue(MAX_SENTENCE_SIZE, maxSentenceSize = QInputDialog::getInt(this, MAX_SENTENCE_SIZE, "", maxSentenceSize, 0, INT_MAX, 1, nullptr, Qt::WindowCloseButtonHint));
|
||||
});
|
||||
|
||||
menu.addAction(CLICK_THROUGH, this, &ExtraWindow::ToggleClickThrough);
|
||||
|
||||
ui.display->installEventFilter(this);
|
||||
ui.display->setMouseTracking(true);
|
||||
qApp->installNativeEventFilter(this);
|
||||
|
||||
QMetaObject::invokeMethod(this, [this]
|
||||
{
|
||||
RegisterHotKey((HWND)winId(), CLICK_THROUGH_HOTKEY, MOD_ALT | MOD_NOREPEAT, 0x58);
|
||||
show();
|
||||
AddSentence(EXTRA_WINDOW_INFO);
|
||||
}, Qt::QueuedConnection);
|
||||
@ -195,36 +234,90 @@ public:
|
||||
|
||||
void AddSentence(QString sentence)
|
||||
{
|
||||
if (sentence.size() > maxSentenceSize) sentence = SENTENCE_TOO_BIG;
|
||||
if (!showOriginal) sentence = sentence.section('\n', sentence.count('\n') / 2 + 1);
|
||||
sanitize(sentence);
|
||||
sentence.chop(std::distance(std::remove(sentence.begin(), sentence.end(), QChar::Tabulation), sentence.end()));
|
||||
sentenceHistory.push_back(sentence);
|
||||
if (sentenceHistory.size() > 1000) sentenceHistory.erase(sentenceHistory.begin());
|
||||
historyIndex = sentenceHistory.size() - 1;
|
||||
ui.display->setText(sentence);
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
private:
|
||||
void setTopmost(bool topmost)
|
||||
void DisplaySentence()
|
||||
{
|
||||
if (sentenceHistory.empty()) return;
|
||||
QString sentence = sentenceHistory[historyIndex];
|
||||
if (sentence.contains(u8"\x200b \n"))
|
||||
if (!showOriginal) sentence = sentence.split(u8"\x200b \n")[1];
|
||||
else if (showOriginalAfterTranslation) sentence = sentence.split(u8"\x200b \n")[1] + "\n" + sentence.split(u8"\x200b \n")[0];
|
||||
|
||||
if (sizeLock && !autoResize)
|
||||
{
|
||||
QFontMetrics fontMetrics(ui.display->font(), ui.display);
|
||||
int low = 0, high = sentence.size(), last = 0;
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = (low + high) / 2;
|
||||
if (fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.left(mid)).height() <= ui.display->height())
|
||||
{
|
||||
last = mid;
|
||||
low = mid + 1;
|
||||
}
|
||||
else high = mid - 1;
|
||||
}
|
||||
sentence = sentence.left(last);
|
||||
}
|
||||
|
||||
ui.display->setText(sentence);
|
||||
if (autoResize)
|
||||
resize(width(), height() - ui.display->height() +
|
||||
QFontMetrics(ui.display->font(), ui.display).boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence).height()
|
||||
);
|
||||
}
|
||||
|
||||
void SetTopmost(bool topmost)
|
||||
{
|
||||
for (auto window : { winId(), dictionaryWindow.winId() })
|
||||
SetWindowPos((HWND)window, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
settings.setValue(TOPMOST, topmost);
|
||||
};
|
||||
|
||||
void setLock(bool locked)
|
||||
void SetPositionLock(bool locked)
|
||||
{
|
||||
settings.setValue(POSITION_LOCK, posLock = locked);
|
||||
};
|
||||
|
||||
void SetSizeLock(bool locked)
|
||||
{
|
||||
setSizeGripEnabled(!locked);
|
||||
settings.setValue(SIZE_LOCK, this->locked = locked);
|
||||
settings.setValue(SIZE_LOCK, sizeLock = locked);
|
||||
};
|
||||
|
||||
void setShowOriginal(bool showOriginal)
|
||||
void SetCenteredText(bool centeredText)
|
||||
{
|
||||
if (!showOriginal && settings.value(SHOW_ORIGINAL, false).toBool()) QMessageBox::information(this, SHOW_ORIGINAL, SHOW_ORIGINAL_INFO);
|
||||
settings.setValue(SHOW_ORIGINAL, this->showOriginal = showOriginal);
|
||||
ui.display->setAlignment(centeredText ? Qt::AlignHCenter : Qt::AlignLeft);
|
||||
settings.setValue(CENTERED_TEXT, this->centeredText = centeredText);
|
||||
};
|
||||
|
||||
void setUseDictionary(bool useDictionary)
|
||||
void SetAutoResize(bool autoResize)
|
||||
{
|
||||
settings.setValue(AUTO_RESIZE_WINDOW_HEIGHT, this->autoResize = autoResize);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetShowOriginal(bool showOriginal)
|
||||
{
|
||||
settings.setValue(SHOW_ORIGINAL, this->showOriginal = showOriginal);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetShowOriginalAfterTranslation(bool showOriginalAfterTranslation)
|
||||
{
|
||||
settings.setValue(ORIGINAL_AFTER_TRANSLATION, this->showOriginalAfterTranslation = showOriginalAfterTranslation);
|
||||
DisplaySentence();
|
||||
};
|
||||
|
||||
void SetUseDictionary(bool useDictionary)
|
||||
{
|
||||
if (useDictionary)
|
||||
{
|
||||
@ -238,27 +331,40 @@ private:
|
||||
settings.setValue(DICTIONARY, this->useDictionary = useDictionary);
|
||||
}
|
||||
|
||||
void computeDictionaryPosition(QPoint mouse)
|
||||
void ToggleClickThrough()
|
||||
{
|
||||
clickThrough = !clickThrough;
|
||||
for (auto window : { winId(), dictionaryWindow.winId() })
|
||||
{
|
||||
unsigned exStyle = GetWindowLongPtrW((HWND)window, GWL_EXSTYLE);
|
||||
if (clickThrough) exStyle |= WS_EX_TRANSPARENT;
|
||||
else exStyle &= ~WS_EX_TRANSPARENT;
|
||||
SetWindowLongPtrW((HWND)window, GWL_EXSTYLE, exStyle);
|
||||
}
|
||||
};
|
||||
|
||||
void ShowDictionary(QPoint mouse)
|
||||
{
|
||||
QString sentence = ui.display->text();
|
||||
const QFont& font = ui.display->font();
|
||||
if (cachedDisplayInfo.compareExchange(ui.display))
|
||||
if (cachedDisplayInfo.CompareExchange(ui.display))
|
||||
{
|
||||
QFontMetrics fontMetrics(font, ui.display);
|
||||
int flags = Qt::TextWordWrap | (ui.display->alignment() & (Qt::AlignLeft | Qt::AlignHCenter));
|
||||
textPositionMap.clear();
|
||||
for (int i = 0, height = 0, lineBreak = 0; i < sentence.size(); ++i)
|
||||
{
|
||||
int block = 1;
|
||||
for (int charHeight = fontMetrics.boundingRect(0, 0, 1, INT_MAX, Qt::TextWordWrap, sentence.mid(i, 1)).height();
|
||||
i + block < sentence.size() && fontMetrics.boundingRect(0, 0, 1, INT_MAX, Qt::TextWordWrap, sentence.mid(i, block + 1)).height() < charHeight * 1.5; ++block);
|
||||
auto boundingRect = fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.left(i + block));
|
||||
for (int charHeight = fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, 1)).height();
|
||||
i + block < sentence.size() && fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, block + 1)).height() < charHeight * 1.5; ++block);
|
||||
auto boundingRect = fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.left(i + block));
|
||||
if (boundingRect.height() > height)
|
||||
{
|
||||
height = boundingRect.height();
|
||||
lineBreak = i;
|
||||
}
|
||||
textPositionMap.push_back({
|
||||
fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.mid(lineBreak, i - lineBreak + 1)).width(),
|
||||
fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.mid(lineBreak, i - lineBreak + 1)).right() + 1,
|
||||
height
|
||||
});
|
||||
}
|
||||
@ -268,20 +374,34 @@ private:
|
||||
if (i == textPositionMap.size() || (mouse - textPositionMap[i]).manhattanLength() > font.pointSize() * 3) return dictionaryWindow.hide();
|
||||
if (sentence.mid(i) == dictionaryWindow.term) return dictionaryWindow.ShowDefinition();
|
||||
dictionaryWindow.ui.display->setFixedWidth(ui.display->width() * 3 / 4);
|
||||
dictionaryWindow.setTerm(sentence.mid(i));
|
||||
dictionaryWindow.SetTerm(sentence.mid(i));
|
||||
int left = i == 0 ? 0 : textPositionMap[i - 1].x(), right = textPositionMap[i].x(),
|
||||
x = textPositionMap[i].x() > ui.display->width() / 2 ? -dictionaryWindow.width() + (right * 3 + left) / 4 : (left * 3 + right) / 4, y = 0;
|
||||
for (auto point : textPositionMap) if (point.y() > y && point.y() < textPositionMap[i].y()) y = point.y();
|
||||
dictionaryWindow.move(ui.display->mapToGlobal(QPoint(x, y - dictionaryWindow.height())));
|
||||
}
|
||||
|
||||
bool nativeEventFilter(const QByteArray&, void* message, long* result) override
|
||||
{
|
||||
auto msg = (MSG*)message;
|
||||
if (msg->message == WM_HOTKEY)
|
||||
if (msg->wParam == CLICK_THROUGH_HOTKEY) return ToggleClickThrough(), true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool eventFilter(QObject*, QEvent* event) override
|
||||
{
|
||||
if (useDictionary && event->type() == QEvent::MouseMove) computeDictionaryPosition(((QMouseEvent*)event)->localPos().toPoint());
|
||||
if (event->type() == QEvent::MouseButtonPress) dictionaryWindow.hide();
|
||||
if (event->type() == QEvent::MouseButtonPress) mousePressEvent((QMouseEvent*)event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void timerEvent(QTimerEvent* event) override
|
||||
{
|
||||
if (useDictionary && QCursor::pos() != oldPos && (!dictionaryWindow.isVisible() || !dictionaryWindow.geometry().contains(QCursor::pos())))
|
||||
ShowDictionary(ui.display->mapFromGlobal(QCursor::pos()));
|
||||
PrettyWindow::timerEvent(event);
|
||||
}
|
||||
|
||||
void mousePressEvent(QMouseEvent* event) override
|
||||
{
|
||||
dictionaryWindow.hide();
|
||||
@ -290,30 +410,31 @@ private:
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* event) override
|
||||
{
|
||||
if (!locked) move(pos() + event->globalPos() - oldPos);
|
||||
if (!posLock) move(pos() + event->globalPos() - oldPos);
|
||||
oldPos = event->globalPos();
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* event) override
|
||||
{
|
||||
int scroll = event->angleDelta().y();
|
||||
if (scroll > 0 && historyIndex > 0) ui.display->setText(sentenceHistory[--historyIndex]);
|
||||
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ui.display->setText(sentenceHistory[++historyIndex]);
|
||||
if (scroll > 0 && historyIndex > 0) --historyIndex;
|
||||
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ++historyIndex;
|
||||
DisplaySentence();
|
||||
}
|
||||
|
||||
bool locked, showOriginal, useDictionary;
|
||||
int maxSentenceSize = 500;
|
||||
bool sizeLock, posLock, centeredText, autoResize, showOriginal, showOriginalAfterTranslation, useDictionary, clickThrough;
|
||||
QPoint oldPos;
|
||||
|
||||
class
|
||||
{
|
||||
public:
|
||||
bool compareExchange(QLabel* display)
|
||||
bool CompareExchange(QLabel* display)
|
||||
{
|
||||
if (display->text() == text && display->font() == font && display->width() == width) return false;
|
||||
if (display->text() == text && display->font() == font && display->width() == width && display->alignment() == alignment) return false;
|
||||
text = display->text();
|
||||
font = display->font();
|
||||
width = display->width();
|
||||
alignment = display->alignment();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -321,6 +442,7 @@ private:
|
||||
QString text;
|
||||
QFont font;
|
||||
int width;
|
||||
Qt::Alignment alignment;
|
||||
} cachedDisplayInfo;
|
||||
std::vector<QPoint> textPositionMap;
|
||||
|
||||
@ -330,8 +452,7 @@ private:
|
||||
class DictionaryWindow : public PrettyWindow
|
||||
{
|
||||
public:
|
||||
DictionaryWindow() :
|
||||
PrettyWindow("Dictionary Window")
|
||||
DictionaryWindow() : PrettyWindow("Dictionary Window")
|
||||
{
|
||||
ui.display->setSizePolicy({ QSizePolicy::Fixed, QSizePolicy::Minimum });
|
||||
}
|
||||
@ -388,7 +509,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void setTerm(QString term)
|
||||
void SetTerm(QString term)
|
||||
{
|
||||
this->term = term;
|
||||
UpdateDictionary();
|
||||
@ -438,14 +559,13 @@ 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;
|
||||
currentInflectionsUsed.push_front(inflection.name);
|
||||
QString root = inflection.root;
|
||||
for (int i = 0; i < root.size(); ++i) if (root[i].isDigit()) root.replace(i, 1, match.captured(root[i].digitValue()));
|
||||
QString root;
|
||||
for (const auto& ch : inflection.root) root += ch.isDigit() ? match.captured(ch.digitValue()) : ch;
|
||||
for (const auto& definition : LookupDefinitions(root, foundDefinitions, currentInflectionsUsed)) results.push_back(definition);
|
||||
}
|
||||
return results;
|
||||
|
@ -10,6 +10,9 @@
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="display">
|
||||
|
@ -1,194 +1,267 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "network.h"
|
||||
#include <ctime>
|
||||
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
|
||||
extern Synchronized<std::wstring> translateTo, apiKey;
|
||||
|
||||
const char* TRANSLATION_PROVIDER = "Google Translate";
|
||||
const char* GET_API_KEY_FROM = "https://codelabs.developers.google.com/codelabs/cloud-translation-intro";
|
||||
QStringList languages
|
||||
const char* GET_API_KEY_FROM = "https://console.cloud.google.com/marketplace/product/google/translate.googleapis.com";
|
||||
extern const QStringList languagesTo
|
||||
{
|
||||
"Afrikaans: af",
|
||||
"Albanian: sq",
|
||||
"Amharic: am",
|
||||
"Arabic: ar",
|
||||
"Armenian: hy",
|
||||
"Azerbaijani: az",
|
||||
"Basque: eu",
|
||||
"Belarusian: be",
|
||||
"Bengali: bn",
|
||||
"Bosnian: bs",
|
||||
"Bulgarian: bg",
|
||||
"Catalan: ca",
|
||||
"Cebuano: ceb",
|
||||
"Chichewa: ny",
|
||||
"Chinese (simplified): zh",
|
||||
"Chinese (traditional): zh-TW",
|
||||
"Corsican: co",
|
||||
"Croatian: hr",
|
||||
"Czech: cs",
|
||||
"Danish: da",
|
||||
"Dutch: nl",
|
||||
"English: en",
|
||||
"Esperanto: eo",
|
||||
"Estonian: et",
|
||||
"Filipino: tl",
|
||||
"Finnish: fi",
|
||||
"French: fr",
|
||||
"Frisian: fy",
|
||||
"Galician: gl",
|
||||
"Georgian: ka",
|
||||
"German: de",
|
||||
"Greek: el",
|
||||
"Gujarati: gu",
|
||||
"Haitian Creole: ht",
|
||||
"Hausa: ha",
|
||||
"Hawaiian: haw",
|
||||
"Hebrew: iw",
|
||||
"Hindi: hi",
|
||||
"Hmong: hmn",
|
||||
"Hungarian: hu",
|
||||
"Icelandic: is",
|
||||
"Igbo: ig",
|
||||
"Indonesian: id",
|
||||
"Irish: ga",
|
||||
"Italian: it",
|
||||
"Japanese: ja",
|
||||
"Javanese: jw",
|
||||
"Kannada: kn",
|
||||
"Kazakh: kk",
|
||||
"Khmer: km",
|
||||
"Kinyarwanda: rw",
|
||||
"Korean: ko",
|
||||
"Kurdish (Kurmanji): ku",
|
||||
"Kyrgyz: ky",
|
||||
"Lao: lo",
|
||||
"Latin: la",
|
||||
"Latvian: lv",
|
||||
"Lithuanian: lt",
|
||||
"Luxembourgish: lb",
|
||||
"Macedonian: mk",
|
||||
"Malagasy: mg",
|
||||
"Malay: ms",
|
||||
"Malayalam: ml",
|
||||
"Maltese: mt",
|
||||
"Maori: mi",
|
||||
"Marathi: mr",
|
||||
"Mongolian: mn",
|
||||
"Myanmar (Burmese): my",
|
||||
"Nepali: ne",
|
||||
"Norwegian: no",
|
||||
"Odia (Oriya): or",
|
||||
"Pashto: ps",
|
||||
"Persian: fa",
|
||||
"Polish: pl",
|
||||
"Portuguese: pt",
|
||||
"Punjabi: pa",
|
||||
"Romanian: ro",
|
||||
"Russian: ru",
|
||||
"Samoan: sm",
|
||||
"Scots Gaelic: gd",
|
||||
"Serbian: sr",
|
||||
"Sesotho: st",
|
||||
"Shona: sn",
|
||||
"Sindhi: sd",
|
||||
"Sinhala: si",
|
||||
"Slovak: sk",
|
||||
"Slovenian: sl",
|
||||
"Somali: so",
|
||||
"Spanish: es",
|
||||
"Sundanese: su",
|
||||
"Swahili: sw",
|
||||
"Swedish: sv",
|
||||
"Tajik: tg",
|
||||
"Tamil: ta",
|
||||
"Tatar: tt",
|
||||
"Telugu: te",
|
||||
"Thai: th",
|
||||
"Turkish: tr",
|
||||
"Turkmen: tk",
|
||||
"Ukrainian: uk",
|
||||
"Urdu: ur",
|
||||
"Uyghur: ug",
|
||||
"Uzbek: uz",
|
||||
"Vietnamese: vi",
|
||||
"Welsh: cy",
|
||||
"Xhosa: xh",
|
||||
"Yiddish: yi",
|
||||
"Yoruba: yo",
|
||||
"Zulu: zu"
|
||||
"Afrikaans",
|
||||
"Albanian",
|
||||
"Amharic",
|
||||
"Arabic",
|
||||
"Armenian",
|
||||
"Azerbaijani",
|
||||
"Basque",
|
||||
"Belarusian",
|
||||
"Bengali",
|
||||
"Bosnian",
|
||||
"Bulgarian",
|
||||
"Catalan",
|
||||
"Cebuano",
|
||||
"Chichewa",
|
||||
"Chinese (Simplified)",
|
||||
"Chinese (Traditional)",
|
||||
"Corsican",
|
||||
"Croatian",
|
||||
"Czech",
|
||||
"Danish",
|
||||
"Dutch",
|
||||
"English",
|
||||
"Esperanto",
|
||||
"Estonian",
|
||||
"Filipino",
|
||||
"Finnish",
|
||||
"French",
|
||||
"Frisian",
|
||||
"Galician",
|
||||
"Georgian",
|
||||
"German",
|
||||
"Greek",
|
||||
"Gujarati",
|
||||
"Haitian Creole",
|
||||
"Hausa",
|
||||
"Hawaiian",
|
||||
"Hebrew",
|
||||
"Hindi",
|
||||
"Hmong",
|
||||
"Hungarian",
|
||||
"Icelandic",
|
||||
"Igbo",
|
||||
"Indonesian",
|
||||
"Irish",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Javanese",
|
||||
"Kannada",
|
||||
"Kazakh",
|
||||
"Khmer",
|
||||
"Kinyarwanda",
|
||||
"Korean",
|
||||
"Kurdish (Kurmanji)",
|
||||
"Kyrgyz",
|
||||
"Lao",
|
||||
"Latin",
|
||||
"Latvian",
|
||||
"Lithuanian",
|
||||
"Luxembourgish",
|
||||
"Macedonian",
|
||||
"Malagasy",
|
||||
"Malay",
|
||||
"Malayalam",
|
||||
"Maltese",
|
||||
"Maori",
|
||||
"Marathi",
|
||||
"Mongolian",
|
||||
"Myanmar (Burmese)",
|
||||
"Nepali",
|
||||
"Norwegian",
|
||||
"Odia (Oriya)",
|
||||
"Pashto",
|
||||
"Persian",
|
||||
"Polish",
|
||||
"Portuguese",
|
||||
"Punjabi",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Samoan",
|
||||
"Scots Gaelic",
|
||||
"Serbian",
|
||||
"Sesotho",
|
||||
"Shona",
|
||||
"Sindhi",
|
||||
"Sinhala",
|
||||
"Slovak",
|
||||
"Slovenian",
|
||||
"Somali",
|
||||
"Spanish",
|
||||
"Sundanese",
|
||||
"Swahili",
|
||||
"Swedish",
|
||||
"Tajik",
|
||||
"Tamil",
|
||||
"Tatar",
|
||||
"Telugu",
|
||||
"Thai",
|
||||
"Turkish",
|
||||
"Turkmen",
|
||||
"Ukrainian",
|
||||
"Urdu",
|
||||
"Uyghur",
|
||||
"Uzbek",
|
||||
"Vietnamese",
|
||||
"Welsh",
|
||||
"Xhosa",
|
||||
"Yiddish",
|
||||
"Yoruba",
|
||||
"Zulu",
|
||||
}, languagesFrom = languagesTo;
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes
|
||||
{
|
||||
{ { L"Afrikaans" }, { L"af" } },
|
||||
{ { L"Albanian" }, { L"sq" } },
|
||||
{ { L"Amharic" }, { L"am" } },
|
||||
{ { L"Arabic" }, { L"ar" } },
|
||||
{ { L"Armenian" }, { L"hy" } },
|
||||
{ { L"Azerbaijani" }, { L"az" } },
|
||||
{ { L"Basque" }, { L"eu" } },
|
||||
{ { L"Belarusian" }, { L"be" } },
|
||||
{ { L"Bengali" }, { L"bn" } },
|
||||
{ { L"Bosnian" }, { L"bs" } },
|
||||
{ { L"Bulgarian" }, { L"bg" } },
|
||||
{ { L"Catalan" }, { L"ca" } },
|
||||
{ { L"Cebuano" }, { L"ceb" } },
|
||||
{ { L"Chichewa" }, { L"ny" } },
|
||||
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
|
||||
{ { L"Chinese (Traditional)" }, { L"zh-TW" } },
|
||||
{ { L"Corsican" }, { L"co" } },
|
||||
{ { L"Croatian" }, { L"hr" } },
|
||||
{ { L"Czech" }, { L"cs" } },
|
||||
{ { L"Danish" }, { L"da" } },
|
||||
{ { L"Dutch" }, { L"nl" } },
|
||||
{ { L"English" }, { L"en" } },
|
||||
{ { L"Esperanto" }, { L"eo" } },
|
||||
{ { L"Estonian" }, { L"et" } },
|
||||
{ { L"Filipino" }, { L"tl" } },
|
||||
{ { L"Finnish" }, { L"fi" } },
|
||||
{ { L"French" }, { L"fr" } },
|
||||
{ { L"Frisian" }, { L"fy" } },
|
||||
{ { L"Galician" }, { L"gl" } },
|
||||
{ { L"Georgian" }, { L"ka" } },
|
||||
{ { L"German" }, { L"de" } },
|
||||
{ { L"Greek" }, { L"el" } },
|
||||
{ { L"Gujarati" }, { L"gu" } },
|
||||
{ { L"Haitian Creole" }, { L"ht" } },
|
||||
{ { L"Hausa" }, { L"ha" } },
|
||||
{ { L"Hawaiian" }, { L"haw" } },
|
||||
{ { L"Hebrew" }, { L"iw" } },
|
||||
{ { L"Hindi" }, { L"hi" } },
|
||||
{ { L"Hmong" }, { L"hmn" } },
|
||||
{ { L"Hungarian" }, { L"hu" } },
|
||||
{ { L"Icelandic" }, { L"is" } },
|
||||
{ { L"Igbo" }, { L"ig" } },
|
||||
{ { L"Indonesian" }, { L"id" } },
|
||||
{ { L"Irish" }, { L"ga" } },
|
||||
{ { L"Italian" }, { L"it" } },
|
||||
{ { L"Japanese" }, { L"ja" } },
|
||||
{ { L"Javanese" }, { L"jw" } },
|
||||
{ { L"Kannada" }, { L"kn" } },
|
||||
{ { L"Kazakh" }, { L"kk" } },
|
||||
{ { L"Khmer" }, { L"km" } },
|
||||
{ { L"Kinyarwanda" }, { L"rw" } },
|
||||
{ { L"Korean" }, { L"ko" } },
|
||||
{ { L"Kurdish (Kurmanji)" }, { L"ku" } },
|
||||
{ { L"Kyrgyz" }, { L"ky" } },
|
||||
{ { L"Lao" }, { L"lo" } },
|
||||
{ { L"Latin" }, { L"la" } },
|
||||
{ { L"Latvian" }, { L"lv" } },
|
||||
{ { L"Lithuanian" }, { L"lt" } },
|
||||
{ { L"Luxembourgish" }, { L"lb" } },
|
||||
{ { L"Macedonian" }, { L"mk" } },
|
||||
{ { L"Malagasy" }, { L"mg" } },
|
||||
{ { L"Malay" }, { L"ms" } },
|
||||
{ { L"Malayalam" }, { L"ml" } },
|
||||
{ { L"Maltese" }, { L"mt" } },
|
||||
{ { L"Maori" }, { L"mi" } },
|
||||
{ { L"Marathi" }, { L"mr" } },
|
||||
{ { L"Mongolian" }, { L"mn" } },
|
||||
{ { L"Myanmar (Burmese)" }, { L"my" } },
|
||||
{ { L"Nepali" }, { L"ne" } },
|
||||
{ { L"Norwegian" }, { L"no" } },
|
||||
{ { L"Odia (Oriya)" }, { L"or" } },
|
||||
{ { L"Pashto" }, { L"ps" } },
|
||||
{ { L"Persian" }, { L"fa" } },
|
||||
{ { L"Polish" }, { L"pl" } },
|
||||
{ { L"Portuguese" }, { L"pt" } },
|
||||
{ { L"Punjabi" }, { L"pa" } },
|
||||
{ { L"Romanian" }, { L"ro" } },
|
||||
{ { L"Russian" }, { L"ru" } },
|
||||
{ { L"Samoan" }, { L"sm" } },
|
||||
{ { L"Scots Gaelic" }, { L"gd" } },
|
||||
{ { L"Serbian" }, { L"sr" } },
|
||||
{ { L"Sesotho" }, { L"st" } },
|
||||
{ { L"Shona" }, { L"sn" } },
|
||||
{ { L"Sindhi" }, { L"sd" } },
|
||||
{ { L"Sinhala" }, { L"si" } },
|
||||
{ { L"Slovak" }, { L"sk" } },
|
||||
{ { L"Slovenian" }, { L"sl" } },
|
||||
{ { L"Somali" }, { L"so" } },
|
||||
{ { L"Spanish" }, { L"es" } },
|
||||
{ { L"Sundanese" }, { L"su" } },
|
||||
{ { L"Swahili" }, { L"sw" } },
|
||||
{ { L"Swedish" }, { L"sv" } },
|
||||
{ { L"Tajik" }, { L"tg" } },
|
||||
{ { L"Tamil" }, { L"ta" } },
|
||||
{ { L"Tatar" }, { L"tt" } },
|
||||
{ { L"Telugu" }, { L"te" } },
|
||||
{ { L"Thai" }, { L"th" } },
|
||||
{ { L"Turkish" }, { L"tr" } },
|
||||
{ { L"Turkmen" }, { L"tk" } },
|
||||
{ { L"Ukrainian" }, { L"uk" } },
|
||||
{ { L"Urdu" }, { L"ur" } },
|
||||
{ { L"Uyghur" }, { L"ug" } },
|
||||
{ { L"Uzbek" }, { L"uz" } },
|
||||
{ { L"Vietnamese" }, { L"vi" } },
|
||||
{ { L"Welsh" }, { L"cy" } },
|
||||
{ { L"Xhosa" }, { L"xh" } },
|
||||
{ { L"Yiddish" }, { L"yi" } },
|
||||
{ { L"Yoruba" }, { L"yo" } },
|
||||
{ { L"Zulu" }, { L"zu" } },
|
||||
{ { L"?" }, { L"auto" } }
|
||||
};
|
||||
|
||||
bool translateSelectedOnly = false, rateLimitAll = true, rateLimitSelected = false, useCache = true;
|
||||
int tokenCount = 30, tokenRestoreDelay = 60000, maxSentenceSize = 500;
|
||||
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
|
||||
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
|
||||
|
||||
unsigned TKK = 0;
|
||||
|
||||
std::wstring GetTranslationUri(const std::wstring& text)
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
|
||||
{
|
||||
// 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))
|
||||
if (!tlp.authKey.empty())
|
||||
{
|
||||
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, SentenceInfo)
|
||||
{
|
||||
if (!apiKey->empty())
|
||||
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&source=" + codes.at(tlp.translateFrom);
|
||||
if (HttpRequest httpRequest{
|
||||
L"Mozilla/5.0 Textractor",
|
||||
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(L"/language/translate/v2?format=text&target=%s&key=%s%s", codes.at(tlp.translateTo), tlp.authKey, translateFromComponent).c_str(),
|
||||
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"GET",
|
||||
FormatString(L"/m?sl=%s&tl=%s&q=%s", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)).c_str()
|
||||
})
|
||||
{
|
||||
// Response formatted as JSON: starts with "[[[" and translation is enclosed in quotes followed by a comma
|
||||
if (httpRequest.response[0] == L'[')
|
||||
{
|
||||
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 };
|
||||
}
|
||||
return { false, FormatString(L"%s (TKK=%u): %s", TRANSLATION_ERROR, _InterlockedExchange(&TKK, 0), httpRequest.response) };
|
||||
auto start = httpRequest.response.find(L"result-container\">"), end = httpRequest.response.find(L'<', start);
|
||||
if (end != std::string::npos) return { true, HTML::Unescape(httpRequest.response.substr(start + 18, end - start - 18)) };
|
||||
return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
|
||||
}
|
||||
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include <fstream>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
extern const char* LUA_INTRO;
|
||||
extern const char* LOAD_LUA_SCRIPT;
|
||||
extern const char* LOAD_SCRIPT;
|
||||
extern const wchar_t* LUA_ERROR;
|
||||
|
||||
constexpr auto LUA_SAVE_FILE = u8"Textractor.lua";
|
||||
@ -41,11 +40,10 @@ bool logErrors = true;
|
||||
Synchronized<std::string> script;
|
||||
std::atomic<int> revCount = 0;
|
||||
|
||||
class Window : public QDialog
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window()
|
||||
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
|
||||
|
||||
@ -56,6 +54,8 @@ public:
|
||||
resize(800, 600);
|
||||
setWindowTitle("Lua");
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
LoadScript();
|
||||
}
|
||||
|
||||
~Window()
|
||||
@ -78,14 +78,14 @@ private:
|
||||
|
||||
QHBoxLayout layout{ this };
|
||||
QPlainTextEdit scriptEditor{ QTextFile(LUA_SAVE_FILE, QIODevice::ReadOnly).readAll(), this };
|
||||
QPushButton loadButton{ LOAD_LUA_SCRIPT, this };
|
||||
QPushButton loadButton{ LOAD_SCRIPT, this };
|
||||
} window;
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -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,11 +17,12 @@ 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_HTTPS_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))
|
||||
{
|
||||
WinHttpReceiveResponse(request, NULL);
|
||||
|
||||
//DWORD size = 0;
|
||||
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &size, WINHTTP_NO_HEADER_INDEX);
|
||||
//this->headers.resize(size);
|
||||
@ -53,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"])")));
|
||||
|
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include <winhttp.h>
|
||||
#include <variant>
|
||||
|
||||
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
|
||||
|
||||
@ -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,
|
||||
@ -29,9 +30,204 @@ struct HttpRequest
|
||||
};
|
||||
|
||||
std::wstring Escape(const std::wstring& text);
|
||||
std::string Escape(const std::string& text);
|
||||
|
||||
namespace HTML
|
||||
{
|
||||
template <typename C>
|
||||
std::basic_string<C> Unescape(std::basic_string<C> text)
|
||||
{
|
||||
constexpr C
|
||||
lt[] = { '&', 'l', 't', ';' },
|
||||
gt[] = { '&', 'g', 't', ';' },
|
||||
apos1[] = { '&', 'a', 'p', 'o', 's', ';' },
|
||||
apos2[] = { '&', '#', '3', '9', ';' },
|
||||
apos3[] = { '&', '#', 'x', '2', '7', ';' },
|
||||
apos4[] = { '&', '#', 'X', '2', '7', ';' },
|
||||
quot[] = { '&', 'q', 'u', 'o', 't', ';' },
|
||||
amp[] = { '&', 'a', 'm', 'p', ';' };
|
||||
for (int i = 0; i < text.size(); ++i)
|
||||
if (text[i] == '&')
|
||||
for (auto [original, length, replacement] : Array<const C*, size_t, C>{
|
||||
{ lt, std::size(lt), '<' },
|
||||
{ gt, std::size(gt), '>' },
|
||||
{ apos1, std::size(apos1), '\'' },
|
||||
{ apos2, std::size(apos2), '\'' },
|
||||
{ apos3, std::size(apos3), '\'' },
|
||||
{ apos4, std::size(apos4), '\'' },
|
||||
{ quot, std::size(quot), '"' },
|
||||
{ amp, std::size(amp), '&' }
|
||||
}) if (std::char_traits<C>::compare(text.data() + i, original, length) == 0) text.replace(i, length, 1, replacement);
|
||||
return 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(), [](C 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(unsigned codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs
|
||||
};
|
||||
|
||||
template <typename 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::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; }
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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[] = { (char)text[i + 2], (char)text[i + 3], (char)text[i + 4], (char)text[i + 5], 0 };
|
||||
unescaped += UTF<C>::FromCodepoint(strtoul(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 {};
|
||||
|
||||
constexpr 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), 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -12,28 +12,28 @@ extern const char* CURRENT_FILTER;
|
||||
const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt";
|
||||
|
||||
std::optional<std::wregex> regex;
|
||||
std::shared_mutex m;
|
||||
DWORD (*GetSelectedProcessId)() = nullptr;
|
||||
std::wstring replace = L"$1";
|
||||
concurrency::reader_writer_lock m;
|
||||
DWORD (*GetSelectedProcessId)() = [] { return 0UL; };
|
||||
|
||||
class Window : public QDialog
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window()
|
||||
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
|
||||
connect(ui.input, &QLineEdit::textEdited, this, &Window::setRegex);
|
||||
connect(ui.save, &QPushButton::clicked, this, &Window::saveRegex);
|
||||
connect(ui.regexEdit, &QLineEdit::textEdited, this, &Window::SetRegex);
|
||||
connect(ui.saveButton, &QPushButton::clicked, this, &Window::Save);
|
||||
|
||||
setWindowTitle(REGEX_FILTER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void setRegex(QString regex)
|
||||
void SetRegex(QString regex)
|
||||
{
|
||||
ui.input->setText(regex);
|
||||
std::lock_guard l(m);
|
||||
ui.regexEdit->setText(regex);
|
||||
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;
|
||||
@ -41,12 +41,12 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void saveRegex()
|
||||
void Save()
|
||||
{
|
||||
auto formatted = FormatString(
|
||||
L"\xfeff|PROCESS|%s|FILTER|%s|END|\r\n",
|
||||
GetModuleFilename(GetSelectedProcessId()).value_or(FormatString(L"Error getting name of process 0x%X", GetSelectedProcessId())),
|
||||
S(ui.input->text())
|
||||
S(ui.regexEdit->text())
|
||||
);
|
||||
std::ofstream(REGEX_SAVE_FILE, std::ios::binary | std::ios::app).write((const char*)formatted.c_str(), formatted.size() * sizeof(wchar_t));
|
||||
}
|
||||
@ -56,17 +56,17 @@ private:
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
static auto _ = GetSelectedProcessId = (DWORD(*)())sentenceInfo["DWORD (*GetSelectedProcessId)()"];
|
||||
static auto _ = GetSelectedProcessId = (DWORD(*)())sentenceInfo["get selected process id"];
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
if (sentenceInfo["current select"] && !regex) if (auto processName = GetModuleFilename(sentenceInfo["process id"]))
|
||||
if (/*sentenceInfo["current select"] && */!regex) if (auto processName = GetModuleFilename(sentenceInfo["process id"]))
|
||||
{
|
||||
std::ifstream stream(REGEX_SAVE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|PROCESS|", L"|FILTER|" });
|
||||
std::vector<std::wstring> regexes;
|
||||
while (auto read = savedFilters.Next()) if (read->at(0) == processName) regexes.push_back(std::move(read->at(1)));
|
||||
if (!regexes.empty()) QMetaObject::invokeMethod(&window, [regex = S(regexes.back())] { window.setRegex(regex); }, Qt::BlockingQueuedConnection);
|
||||
if (!regexes.empty()) QMetaObject::invokeMethod(&window, std::bind(&Window::SetRegex, &window, S(regexes.back())), Qt::BlockingQueuedConnection);
|
||||
}
|
||||
std::shared_lock l(m);
|
||||
if (regex) sentence = std::regex_replace(sentence, regex.value(), L"");
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
if (regex) sentence = std::regex_replace(sentence, regex.value(), replace);
|
||||
return true;
|
||||
}
|
||||
|
@ -6,13 +6,24 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>350</width>
|
||||
<height>105</height>
|
||||
<width>500</width>
|
||||
<height>80</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="input"/>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="regexEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="saveButton">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="output">
|
||||
@ -24,13 +35,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="save">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel">
|
||||
<property name="text">
|
||||
|
72
extensions/regexreplacer.cpp
Normal file
72
extensions/regexreplacer.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
#include "extension.h"
|
||||
#include "module.h"
|
||||
#include "blockmarkup.h"
|
||||
#include <fstream>
|
||||
#include <process.h>
|
||||
|
||||
extern const wchar_t* REGEX_REPLACER_INSTRUCTIONS;
|
||||
|
||||
const char* REPLACE_SAVE_FILE = "SavedRegexReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
concurrency::reader_writer_lock m;
|
||||
std::vector<std::tuple<std::wregex, std::wstring, std::regex_constants::match_flag_type>> replacements;
|
||||
|
||||
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 lock(m);
|
||||
replacements.clear();
|
||||
std::ifstream stream(REPLACE_SAVE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|REGEX|", L"|BECOMES|", L"|MODIFIER|" });
|
||||
while (auto read = savedFilters.Next())
|
||||
{
|
||||
const auto& [regex, replacement, modifier] = read.value();
|
||||
try
|
||||
{
|
||||
replacements.emplace_back(
|
||||
std::wregex(regex, modifier.find(L'i') == std::string::npos ? std::regex::ECMAScript : std::regex::icase),
|
||||
replacement,
|
||||
modifier.find(L'g') == std::string::npos ? std::regex_constants::format_first_only : std::regex_constants::format_default
|
||||
);
|
||||
}
|
||||
catch (std::regex_error) {}
|
||||
}
|
||||
}
|
||||
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
{
|
||||
UpdateReplacements();
|
||||
if (replacements.empty())
|
||||
{
|
||||
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
|
||||
for (auto ch : std::wstring_view(REGEX_REPLACER_INSTRUCTIONS))
|
||||
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
|
||||
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
for (const auto& [regex, replacement, flags] : replacements) sentence = std::regex_replace(sentence, regex, replacement, flags);
|
||||
return true;
|
||||
}
|
@ -5,30 +5,25 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
std::vector<int> repeatNumbers(sentence.size() + 1, 0);
|
||||
int repeatNumber = 1;
|
||||
wchar_t prevChar = L'\0';
|
||||
for (auto nextChar : sentence)
|
||||
for (int i = 0; i < sentence.size(); ++i)
|
||||
{
|
||||
if (nextChar == prevChar)
|
||||
if (sentence[i] != sentence[i + 1])
|
||||
{
|
||||
repeatNumber += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevChar = nextChar;
|
||||
repeatNumbers.at(repeatNumber) += 1;
|
||||
repeatNumber = 1;
|
||||
int j = i;
|
||||
while (sentence[j] == sentence[i] && --j >= 0);
|
||||
repeatNumbers[i - j] += 1;
|
||||
}
|
||||
}
|
||||
if ((repeatNumber = std::distance(repeatNumbers.begin(), std::max_element(repeatNumbers.begin(), repeatNumbers.end()))) == 1) return false;
|
||||
int repeatNumber = std::distance(repeatNumbers.begin(), std::max_element(repeatNumbers.rbegin(), repeatNumbers.rend()).base() - 1);
|
||||
if (repeatNumber < 2) return false;
|
||||
|
||||
std::wstring newSentence;
|
||||
for (int i = 0; i < sentence.size();)
|
||||
{
|
||||
newSentence.push_back(sentence.at(i));
|
||||
newSentence.push_back(sentence[i]);
|
||||
for (int j = i; j <= sentence.size(); ++j)
|
||||
{
|
||||
if (j == sentence.size() || sentence.at(i) != sentence.at(j))
|
||||
if (j == sentence.size() || sentence[i] != sentence[j])
|
||||
{
|
||||
i += (j - i) % repeatNumber == 0 ? repeatNumber : 1;
|
||||
break;
|
||||
@ -44,8 +39,11 @@ TEST(
|
||||
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
|
||||
|
||||
std::wstring repeatedChars = L"aaaaaaaaaaaabbbbbbcccdddaabbbcccddd";
|
||||
std::wstring someRepeatedChars = L"abcdefaabbccddeeff";
|
||||
ProcessSentence(repeatedChars, { nonConsole });
|
||||
ProcessSentence(someRepeatedChars, { nonConsole });
|
||||
assert(repeatedChars.find(L"aaaabbcd") == 0);
|
||||
assert(someRepeatedChars == L"abcdefabcdef");
|
||||
|
||||
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
|
||||
ProcessSentence(empty, { nonConsole });
|
||||
|
@ -16,10 +16,10 @@ std::vector<int> GenerateSuffixArray(const std::wstring& text)
|
||||
eqClasses[suffixArray[0]] = 0;
|
||||
for (int i = 1; i < text.size(); ++i)
|
||||
{
|
||||
int currentSuffix = suffixArray[i];
|
||||
int lastSuffix = suffixArray[i - 1];
|
||||
int currentSuffix = suffixArray[i], lastSuffix = suffixArray[i - 1];
|
||||
if (currentSuffix + length < text.size() && prevEqClasses[currentSuffix] == prevEqClasses[lastSuffix] &&
|
||||
prevEqClasses[currentSuffix + length / 2] == prevEqClasses.at(lastSuffix + length / 2)) // not completely certain that this will stay in range
|
||||
prevEqClasses[currentSuffix + length / 2] == prevEqClasses[lastSuffix + length / 2]
|
||||
)
|
||||
eqClasses[currentSuffix] = eqClasses[lastSuffix];
|
||||
else eqClasses[currentSuffix] = i;
|
||||
}
|
||||
@ -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]));
|
||||
|
@ -10,7 +10,7 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
|
||||
{
|
||||
wchar_t filePath[MAX_PATH];
|
||||
GetModuleFileNameW(hModule, filePath, MAX_PATH);
|
||||
if (wchar_t* fileName = wcsrchr(filePath, L'\\')) swscanf_s(fileName, L"\\Remove %d Repeated Sentences.dll", &sentenceCacheSize);
|
||||
if (wchar_t* fileName = wcsrchr(filePath, L'\\')) swscanf_s(fileName, L"\\Remove %d Repeated Sentences.xdll", &sentenceCacheSize);
|
||||
}
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
@ -30,7 +30,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
static std::mutex m;
|
||||
m.lock();
|
||||
if (textNumber + 1 > cache.size()) cache.resize(textNumber + 1);
|
||||
auto prevSentences = cache.at(textNumber).Acquire();
|
||||
auto prevSentences = cache[textNumber].Acquire();
|
||||
m.unlock();
|
||||
auto& inserted = prevSentences->emplace_back(sentence);
|
||||
auto firstLocation = std::find(prevSentences->begin(), prevSentences->end(), sentence);
|
||||
|
@ -10,7 +10,7 @@ extern const wchar_t* REPLACER_INSTRUCTIONS;
|
||||
constexpr auto REPLACE_SAVE_FILE = u8"SavedReplacements.txt";
|
||||
|
||||
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
|
||||
std::shared_mutex m;
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Trie
|
||||
{
|
||||
@ -61,7 +61,7 @@ public:
|
||||
private:
|
||||
static bool Ignore(wchar_t ch)
|
||||
{
|
||||
return ch <= 0x20 || std::iswspace(ch);
|
||||
return ch <= 0x20 || iswspace(ch);
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
@ -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,8 +103,9 @@ 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));
|
||||
_spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); // show file to user
|
||||
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));
|
||||
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -120,7 +121,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo)
|
||||
{
|
||||
UpdateReplacements();
|
||||
|
||||
std::shared_lock l(m);
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
sentence = trie.Replace(sentence);
|
||||
return true;
|
||||
}
|
||||
|
54
extensions/styler.cpp
Normal file
54
extensions/styler.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
extern const char* LOAD_SCRIPT;
|
||||
|
||||
constexpr auto STYLE_SAVE_FILE = u8"Textractor.qss";
|
||||
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
|
||||
|
||||
if (scriptEditor.toPlainText().isEmpty())
|
||||
scriptEditor.setPlainText("/*\nhttps://www.google.com/search?q=Qt+stylesheet+gallery\nhttps://doc.qt.io/qt-5/stylesheet-syntax.html\n*/");
|
||||
layout.addWidget(&scriptEditor);
|
||||
layout.addWidget(&loadButton);
|
||||
|
||||
resize(800, 600);
|
||||
setWindowTitle("Styler");
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
LoadScript();
|
||||
}
|
||||
|
||||
~Window()
|
||||
{
|
||||
qApp->setStyleSheet("");
|
||||
Save();
|
||||
}
|
||||
|
||||
private:
|
||||
void LoadScript()
|
||||
{
|
||||
qApp->setStyleSheet(scriptEditor.toPlainText());
|
||||
Save();
|
||||
}
|
||||
|
||||
void Save()
|
||||
{
|
||||
QTextFile(STYLE_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
|
||||
}
|
||||
|
||||
QHBoxLayout layout{ this };
|
||||
QPlainTextEdit scriptEditor{ QTextFile(STYLE_SAVE_FILE, QIODevice::ReadOnly).readAll(), this };
|
||||
QPushButton loadButton{ LOAD_SCRIPT, this };
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
@ -1,26 +1,30 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "ui_threadlinker.h"
|
||||
#include <QKeyEvent>
|
||||
|
||||
extern const char* THREAD_LINKER;
|
||||
extern const char* LINK;
|
||||
extern const char* UNLINK;
|
||||
extern const char* THREAD_LINK_FROM;
|
||||
extern const char* THREAD_LINK_TO;
|
||||
extern const char* HEXADECIMAL;
|
||||
|
||||
std::unordered_map<int64_t, std::unordered_multiset<int64_t>> linkedTextHandles;
|
||||
std::shared_mutex m;
|
||||
std::unordered_map<int64_t, std::unordered_set<int64_t>> links;
|
||||
std::unordered_set<int64_t> universalLinks, empty;
|
||||
bool separateSentences = false; // allow user to change?
|
||||
concurrency::reader_writer_lock m;
|
||||
|
||||
class Window : public QDialog
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window()
|
||||
: QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
connect(&linkButton, &QPushButton::clicked, this, &Window::Link);
|
||||
|
||||
layout.addWidget(&linkList);
|
||||
layout.addWidget(&linkButton);
|
||||
ui.setupUi(this);
|
||||
ui.linkButton->setText(LINK);
|
||||
ui.unlinkButton->setText(UNLINK);
|
||||
connect(ui.linkButton, &QPushButton::clicked, this, &Window::Link);
|
||||
connect(ui.unlinkButton, &QPushButton::clicked, this, &Window::Unlink);
|
||||
|
||||
setWindowTitle(THREAD_LINKER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
@ -30,39 +34,46 @@ private:
|
||||
void Link()
|
||||
{
|
||||
bool ok1, ok2, ok3, ok4;
|
||||
int from = QInputDialog::getText(this, THREAD_LINK_FROM, HEXADECIMAL, QLineEdit::Normal, "", &ok1, Qt::WindowCloseButtonHint).toInt(&ok2, 16);
|
||||
int to = QInputDialog::getText(this, THREAD_LINK_TO, HEXADECIMAL, QLineEdit::Normal, "", &ok3, Qt::WindowCloseButtonHint).toInt(&ok4, 16);
|
||||
if (ok1 && ok2 && ok3 && ok4)
|
||||
QString fromInput = QInputDialog::getText(this, THREAD_LINK_FROM, HEXADECIMAL, QLineEdit::Normal, "All", &ok1, Qt::WindowCloseButtonHint);
|
||||
int from = fromInput.toInt(&ok2, 16);
|
||||
if (ok1 && (fromInput == "All" || ok2))
|
||||
{
|
||||
std::lock_guard l(m);
|
||||
linkedTextHandles[from].insert(to);
|
||||
linkList.addItem(QString::number(from, 16) + "->" + QString::number(to, 16));
|
||||
int to = QInputDialog::getText(this, THREAD_LINK_TO, HEXADECIMAL, QLineEdit::Normal, "", &ok3, Qt::WindowCloseButtonHint).toInt(&ok4, 16);
|
||||
if (ok3 && ok4)
|
||||
{
|
||||
std::scoped_lock lock(m);
|
||||
if ((ok2 ? links[from] : universalLinks).insert(to).second)
|
||||
ui.linkList->addItem((ok2 ? QString::number(from, 16) : "All") + "->" + QString::number(to, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Unlink()
|
||||
{
|
||||
if (ui.linkList->currentItem())
|
||||
{
|
||||
QStringList link = ui.linkList->currentItem()->text().split("->");
|
||||
ui.linkList->takeItem(ui.linkList->currentRow());
|
||||
std::scoped_lock lock(m);
|
||||
(link[0] == "All" ? universalLinks : links[link[0].toInt(nullptr, 16)]).erase(link[1].toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
void keyPressEvent(QKeyEvent* event) override
|
||||
{
|
||||
if (event->key() == Qt::Key_Delete && linkList.currentItem())
|
||||
{
|
||||
QStringList link = linkList.currentItem()->text().split("->");
|
||||
linkList.takeItem(linkList.currentRow());
|
||||
std::lock_guard l(m);
|
||||
linkedTextHandles[link[0].toInt(nullptr, 16)].erase(link[1].toInt(nullptr, 16));
|
||||
}
|
||||
if (event->key() == Qt::Key_Delete) Unlink();
|
||||
}
|
||||
|
||||
QHBoxLayout layout{ this };
|
||||
QListWidget linkList{ this };
|
||||
QPushButton linkButton{ LINK, this };
|
||||
Ui::LinkWindow ui;
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
std::shared_lock l(m);
|
||||
int64_t textHandle = sentenceInfo["text number"];
|
||||
|
||||
for (auto linkedHandle : linkedTextHandles[textHandle])
|
||||
((void(*)(int64_t, const wchar_t*))sentenceInfo["void (*AddSentence)(int64_t number, const wchar_t* sentence)"])(linkedHandle, sentence.c_str());
|
||||
|
||||
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
|
||||
auto action = separateSentences ? sentenceInfo["add sentence"] : sentenceInfo["add text"];
|
||||
auto it = links.find(sentenceInfo["text number"]);
|
||||
for (const auto& linkSet : { it != links.end() ? it->second : empty, sentenceInfo["text number"] > 1 ? universalLinks : empty })
|
||||
for (auto link : linkSet)
|
||||
((void(*)(int64_t, const wchar_t*))action)(link, sentence.c_str());
|
||||
return false;
|
||||
}
|
||||
|
47
extensions/threadlinker.ui
Normal file
47
extensions/threadlinker.ui
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>LinkWindow</class>
|
||||
<widget class="QDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="linkList"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="linkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="unlinkButton">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -1,74 +1,102 @@
|
||||
#include "qtcommon.h"
|
||||
#include "extension.h"
|
||||
#include "translatewrapper.h"
|
||||
#include "blockmarkup.h"
|
||||
#include "network.h"
|
||||
#include <map>
|
||||
#include <concurrent_priority_queue.h>
|
||||
#include <fstream>
|
||||
#include <QComboBox>
|
||||
|
||||
extern const char* NATIVE_LANGUAGE;
|
||||
extern const char* TRANSLATE_TO;
|
||||
extern const char* TRANSLATE_FROM;
|
||||
extern const char* TRANSLATE_SELECTED_THREAD_ONLY;
|
||||
extern const char* RATE_LIMIT_ALL_THREADS;
|
||||
extern const char* RATE_LIMIT_SELECTED_THREAD;
|
||||
extern const char* USE_TRANS_CACHE;
|
||||
extern const char* RATE_LIMIT_TOKEN_COUNT;
|
||||
extern const char* RATE_LIMIT_TOKEN_RESTORE_DELAY;
|
||||
extern const char* FILTER_GARBAGE;
|
||||
extern const char* MAX_TRANSLATIONS_IN_TIMESPAN;
|
||||
extern const char* TIMESPAN;
|
||||
extern const char* MAX_SENTENCE_SIZE;
|
||||
extern const char* API_KEY;
|
||||
extern const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
extern const wchar_t* TRANSLATION_ERROR;
|
||||
extern const wchar_t* TOO_MANY_TRANS_REQUESTS;
|
||||
|
||||
extern const char* TRANSLATION_PROVIDER;
|
||||
extern const char* GET_API_KEY_FROM;
|
||||
extern QStringList languages;
|
||||
extern bool translateSelectedOnly, rateLimitAll, rateLimitSelected, useCache;
|
||||
extern int tokenCount, tokenRestoreDelay, maxSentenceSize;
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, SentenceInfo sentenceInfo);
|
||||
|
||||
const char* LANGUAGE = u8"Language";
|
||||
const std::string TRANSLATION_CACHE_FILE = FormatString("%s Cache.txt", TRANSLATION_PROVIDER);
|
||||
extern const QStringList languagesTo, languagesFrom;
|
||||
extern bool translateSelectedOnly, useRateLimiter, rateLimitSelected, useCache, useFilter;
|
||||
extern int tokenCount, rateLimitTimespan, maxSentenceSize;
|
||||
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp);
|
||||
|
||||
QFormLayout* display;
|
||||
QSettings settings = openSettings();
|
||||
Synchronized<std::wstring> translateTo = L"en", apiKey;
|
||||
Settings settings;
|
||||
|
||||
Synchronized<std::map<std::wstring, std::wstring>> translationCache;
|
||||
int savedSize;
|
||||
|
||||
void SaveCache()
|
||||
namespace
|
||||
{
|
||||
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();
|
||||
Synchronized<TranslationParam> tlp;
|
||||
Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
|
||||
|
||||
std::string CacheFile()
|
||||
{
|
||||
return FormatString("%s Cache (%S).txt", TRANSLATION_PROVIDER, tlp->translateTo);
|
||||
}
|
||||
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(CacheFile(), std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
|
||||
}
|
||||
void LoadCache()
|
||||
{
|
||||
translationCache->clear();
|
||||
std::ifstream stream(CacheFile(), std::ios::binary);
|
||||
BlockMarkupIterator savedTranslations(stream, Array<std::wstring_view>{ L"|SENTENCE|", L"|TRANSLATION|" });
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
while (auto read = savedTranslations.Next())
|
||||
{
|
||||
auto& [sentence, translation] = read.value();
|
||||
translationCache->try_emplace(std::move(sentence), std::move(translation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Window : public QDialog
|
||||
class Window : public QDialog, Localizer
|
||||
{
|
||||
public:
|
||||
Window() :
|
||||
QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
|
||||
{
|
||||
display = new QFormLayout(this);
|
||||
|
||||
settings.beginGroup(TRANSLATION_PROVIDER);
|
||||
|
||||
auto languageBox = new QComboBox(this);
|
||||
languageBox->addItems(languages);
|
||||
int language = -1;
|
||||
if (settings.contains(LANGUAGE)) language = languageBox->findText(settings.value(LANGUAGE).toString(), Qt::MatchEndsWith);
|
||||
if (language < 0) language = languageBox->findText(NATIVE_LANGUAGE, Qt::MatchStartsWith);
|
||||
if (language < 0) language = languageBox->findText("English", Qt::MatchStartsWith);
|
||||
languageBox->setCurrentIndex(language);
|
||||
saveLanguage(languageBox->currentText());
|
||||
display->addRow(TRANSLATE_TO, languageBox);
|
||||
connect(languageBox, &QComboBox::currentTextChanged, this, &Window::saveLanguage);
|
||||
auto translateToCombo = new QComboBox(this);
|
||||
translateToCombo->addItems(languagesTo);
|
||||
int i = -1;
|
||||
if (settings.contains(TRANSLATE_TO)) i = translateToCombo->findText(settings.value(TRANSLATE_TO).toString());
|
||||
if (i < 0) i = translateToCombo->findText(NATIVE_LANGUAGE, Qt::MatchStartsWith);
|
||||
if (i < 0) i = translateToCombo->findText("English", Qt::MatchStartsWith);
|
||||
translateToCombo->setCurrentIndex(i);
|
||||
SaveTranslateTo(translateToCombo->currentText());
|
||||
display->addRow(TRANSLATE_TO, translateToCombo);
|
||||
connect(translateToCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateTo);
|
||||
auto translateFromCombo = new QComboBox(this);
|
||||
translateFromCombo->addItem("?");
|
||||
translateFromCombo->addItems(languagesFrom);
|
||||
i = -1;
|
||||
if (settings.contains(TRANSLATE_FROM)) i = translateFromCombo->findText(settings.value(TRANSLATE_FROM).toString());
|
||||
if (i < 0) i = 0;
|
||||
translateFromCombo->setCurrentIndex(i);
|
||||
SaveTranslateFrom(translateFromCombo->currentText());
|
||||
display->addRow(TRANSLATE_FROM, translateFromCombo);
|
||||
connect(translateFromCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateFrom);
|
||||
for (auto [value, label] : Array<bool&, const char*>{
|
||||
{ translateSelectedOnly, TRANSLATE_SELECTED_THREAD_ONLY },
|
||||
{ rateLimitAll, RATE_LIMIT_ALL_THREADS },
|
||||
{ useRateLimiter, RATE_LIMIT_ALL_THREADS },
|
||||
{ rateLimitSelected, RATE_LIMIT_SELECTED_THREAD },
|
||||
{ useCache, USE_TRANS_CACHE },
|
||||
{ useFilter, FILTER_GARBAGE }
|
||||
})
|
||||
{
|
||||
value = settings.value(label, value).toBool();
|
||||
@ -78,8 +106,8 @@ public:
|
||||
connect(checkBox, &QCheckBox::clicked, [label, &value](bool checked) { settings.setValue(label, value = checked); });
|
||||
}
|
||||
for (auto [value, label] : Array<int&, const char*>{
|
||||
{ tokenCount, RATE_LIMIT_TOKEN_COUNT },
|
||||
{ tokenRestoreDelay, RATE_LIMIT_TOKEN_RESTORE_DELAY },
|
||||
{ tokenCount, MAX_TRANSLATIONS_IN_TIMESPAN },
|
||||
{ rateLimitTimespan, TIMESPAN },
|
||||
{ maxSentenceSize, MAX_SENTENCE_SIZE },
|
||||
})
|
||||
{
|
||||
@ -92,26 +120,16 @@ public:
|
||||
}
|
||||
if (GET_API_KEY_FROM)
|
||||
{
|
||||
auto keyInput = new QLineEdit(settings.value(API_KEY).toString());
|
||||
apiKey->assign(S(keyInput->text()));
|
||||
QObject::connect(keyInput, &QLineEdit::textChanged, [](QString key) { settings.setValue(API_KEY, S(apiKey->assign(S(key)))); });
|
||||
auto keyEdit = new QLineEdit(settings.value(API_KEY).toString(), this);
|
||||
tlp->authKey = S(keyEdit->text());
|
||||
QObject::connect(keyEdit, &QLineEdit::textChanged, [](QString key) { settings.setValue(API_KEY, S(tlp->authKey = S(key))); });
|
||||
auto keyLabel = new QLabel(QString("<a href=\"%1\">%2</a>").arg(GET_API_KEY_FROM, API_KEY), this);
|
||||
keyLabel->setOpenExternalLinks(true);
|
||||
display->addRow(keyLabel, keyInput);
|
||||
display->addRow(keyLabel, keyEdit);
|
||||
}
|
||||
|
||||
setWindowTitle(TRANSLATION_PROVIDER);
|
||||
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
|
||||
|
||||
std::ifstream stream(TRANSLATION_CACHE_FILE, std::ios::binary);
|
||||
BlockMarkupIterator savedTranslations(stream, Array<std::wstring_view>{ L"|SENTENCE|", L"|TRANSLATION|" });
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
while (auto read = savedTranslations.Next())
|
||||
{
|
||||
auto& [sentence, translation] = read.value();
|
||||
translationCache->try_emplace(std::move(sentence), std::move(translation));
|
||||
}
|
||||
savedSize = translationCache->size();
|
||||
}
|
||||
|
||||
~Window()
|
||||
@ -120,48 +138,75 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void saveLanguage(QString language)
|
||||
void SaveTranslateTo(QString language)
|
||||
{
|
||||
settings.setValue(LANGUAGE, S(translateTo->assign(S(language.split(": ")[1]))));
|
||||
SaveCache();
|
||||
settings.setValue(TRANSLATE_TO, S(tlp->translateTo = S(language)));
|
||||
LoadCache();
|
||||
}
|
||||
void SaveTranslateFrom(QString language)
|
||||
{
|
||||
settings.setValue(TRANSLATE_FROM, S(tlp->translateFrom = S(language)));
|
||||
}
|
||||
} window;
|
||||
|
||||
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
|
||||
{
|
||||
if (sentenceInfo["text number"] == 0 || sentence.size() > maxSentenceSize) return false;
|
||||
if (sentenceInfo["text number"] == 0) return false;
|
||||
|
||||
static class
|
||||
{
|
||||
public:
|
||||
bool Request()
|
||||
{
|
||||
auto tokens = this->tokens.Acquire();
|
||||
tokens->push_back(GetTickCount());
|
||||
if (tokens->size() > tokenCount * 5) tokens->erase(tokens->begin(), tokens->begin() + tokenCount * 3);
|
||||
tokens->erase(std::remove_if(tokens->begin(), tokens->end(), [](DWORD token) { return GetTickCount() - token > tokenRestoreDelay; }), tokens->end());
|
||||
return tokens->size() < tokenCount;
|
||||
DWORD64 current = GetTickCount64(), token;
|
||||
while (tokens.try_pop(token)) if (token > current - rateLimitTimespan)
|
||||
{
|
||||
tokens.push(token); // popped one too many
|
||||
break;
|
||||
}
|
||||
bool available = tokens.size() < tokenCount;
|
||||
if (available) tokens.push(current);
|
||||
return available;
|
||||
}
|
||||
|
||||
private:
|
||||
Synchronized<std::vector<DWORD>> tokens;
|
||||
concurrency::concurrent_priority_queue<DWORD64, std::greater<DWORD64>> tokens;
|
||||
} rateLimiter;
|
||||
|
||||
bool cache = false;
|
||||
std::wstring translation;
|
||||
if (useFilter)
|
||||
{
|
||||
Trim(sentence);
|
||||
sentence.erase(std::remove_if(sentence.begin(), sentence.end(), [](wchar_t ch) { return ch < ' ' && ch != '\n'; }), sentence.end());
|
||||
}
|
||||
if (sentence.empty()) return true;
|
||||
if (sentence.size() > maxSentenceSize) translation = SENTENCE_TOO_LARGE_TO_TRANS;
|
||||
if (useCache)
|
||||
{
|
||||
auto translationCache = ::translationCache.Acquire();
|
||||
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second + L"\x200b"; // dumb hack to not try to translate if stored empty translation
|
||||
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second;
|
||||
}
|
||||
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
|
||||
if (rateLimiter.Request() || !rateLimitAll || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence, sentenceInfo);
|
||||
if (rateLimiter.Request() || !useRateLimiter || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence, tlp.Copy());
|
||||
else translation = TOO_MANY_TRANS_REQUESTS;
|
||||
if (cache) translationCache->try_emplace(sentence, translation);
|
||||
if (cache && translationCache->size() > savedSize + 50) SaveCache();
|
||||
if (cache) translationCache->operator[](sentence) = translation;
|
||||
|
||||
JSON::Unescape(translation);
|
||||
sentence += L"\n" + translation;
|
||||
if (useFilter) Trim(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()) translation = TRANSLATION_ERROR;
|
||||
(sentence += L"\x200b \n") += translation;
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(assert(Translate(L"こんにちは").second.find(L"ello") != std::wstring::npos));
|
||||
extern const std::unordered_map<std::wstring, std::wstring> codes;
|
||||
TEST(
|
||||
{
|
||||
assert(Translate(L"こんにちは", { L"English", L"?", L"" }).second.find(L"ello") == 1 || strstr(TRANSLATION_PROVIDER, "DevTools"));
|
||||
|
||||
for (auto languages : { languagesFrom, languagesTo }) for (auto language : languages)
|
||||
assert(codes.count(S(language)));
|
||||
assert(codes.count(L"?"));
|
||||
}
|
||||
);
|
||||
|
6
extensions/translatewrapper.h
Normal file
6
extensions/translatewrapper.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
struct TranslationParam
|
||||
{
|
||||
std::wstring translateTo, translateFrom, authKey;
|
||||
};
|
4
host/CLI/CMakeLists.txt
Normal file
4
host/CLI/CMakeLists.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# The CLI isn't used by Textractor itself, but is here for other people that want to build projects on top of Textractor
|
||||
add_executable(TextractorCLI main.cpp)
|
||||
target_precompile_headers(TextractorCLI REUSE_FROM pch)
|
||||
target_link_libraries(TextractorCLI host)
|
@ -1,5 +1,5 @@
|
||||
#include "host.h"
|
||||
#include "hookcode.h"
|
||||
#include "../host.h"
|
||||
#include "../hookcode.h"
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
8
host/CMakeLists.txt
Normal file
8
host/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
add_library(host
|
||||
host.cpp
|
||||
textthread.cpp
|
||||
hookcode.cpp
|
||||
)
|
||||
target_precompile_headers(host REUSE_FROM pch)
|
||||
|
||||
add_subdirectory(CLI)
|
@ -88,7 +88,7 @@ namespace
|
||||
}
|
||||
HCode.erase(0, 1);
|
||||
|
||||
if ((hp.type & USING_STRING))
|
||||
if (hp.type & USING_STRING)
|
||||
{
|
||||
if (HCode[0] == L'F')
|
||||
{
|
||||
@ -125,36 +125,43 @@ namespace
|
||||
HCode.erase(0, match[0].length());
|
||||
}
|
||||
|
||||
auto ConsumeHexInt = [&HCode]
|
||||
{
|
||||
size_t size = 0;
|
||||
int value = 0;
|
||||
try { value = std::stoi(HCode, &size, 16); } catch (std::invalid_argument) {}
|
||||
HCode.erase(0, size);
|
||||
return value;
|
||||
};
|
||||
|
||||
// data_offset
|
||||
if (!std::regex_search(HCode, match, std::wregex(L"^-?[[:xdigit:]]+"))) return {};
|
||||
hp.offset = std::stoi(match[0], nullptr, 16);
|
||||
HCode.erase(0, match[0].length());
|
||||
hp.offset = ConsumeHexInt();
|
||||
|
||||
// [*deref_offset1]
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^\\*(-?[[:xdigit:]]+)")))
|
||||
if (HCode[0] == L'*')
|
||||
{
|
||||
hp.type |= DATA_INDIRECT;
|
||||
hp.index = std::stoi(match[1], nullptr, 16);
|
||||
HCode.erase(0, match[0].length());
|
||||
HCode.erase(0, 1);
|
||||
hp.index = ConsumeHexInt();
|
||||
}
|
||||
|
||||
// [:split_offset[*deref_offset2]]
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^:(-?[[:xdigit:]]+)")))
|
||||
if (HCode[0] == L':')
|
||||
{
|
||||
hp.type |= USING_SPLIT;
|
||||
hp.split = std::stoi(match[1], nullptr, 16);
|
||||
HCode.erase(0, match[0].length());
|
||||
HCode.erase(0, 1);
|
||||
hp.split = ConsumeHexInt();
|
||||
|
||||
if (std::regex_search(HCode, match, std::wregex(L"^\\*(-?[[:xdigit:]]+)")))
|
||||
if (HCode[0] == L'*')
|
||||
{
|
||||
hp.type |= SPLIT_INDIRECT;
|
||||
hp.split_index = std::stoi(match[1], nullptr, 16);
|
||||
HCode.erase(0, match[0].length());
|
||||
HCode.erase(0, 1);
|
||||
hp.split_index = ConsumeHexInt();
|
||||
}
|
||||
}
|
||||
|
||||
// @addr[:module[:func]]
|
||||
if (!std::regex_match(HCode, match, std::wregex(L"@([[:xdigit:]]+)(:.+?)?(:.+)?"))) return {};
|
||||
if (!std::regex_match(HCode, match, std::wregex(L"^@([[:xdigit:]]+)(:.+?)?(:.+)?"))) return {};
|
||||
hp.address = std::stoull(match[1], nullptr, 16);
|
||||
if (match[2].matched)
|
||||
{
|
||||
@ -269,7 +276,9 @@ namespace HookCode
|
||||
{
|
||||
std::optional<HookParam> Parse(std::wstring code)
|
||||
{
|
||||
if (code[0] == L'/') code.erase(0, 1); // legacy/AGTH compatibility
|
||||
if (code[0] == L'/') code.erase(0, 1);
|
||||
code.erase(std::find(code.begin(), code.end(), L'/'), code.end()); // legacy/AGTH compatibility
|
||||
Trim(code);
|
||||
if (code[0] == L'R') return ParseRCode(code.erase(0, 1));
|
||||
else if (code[0] == L'H') return ParseHCode(code.erase(0, 1));
|
||||
return {};
|
||||
@ -285,9 +294,10 @@ namespace HookCode
|
||||
assert(HexString(-12) == L"-C"),
|
||||
assert(HexString(12) == L"C"),
|
||||
assert(Parse(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA")),
|
||||
assert(Parse(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA /KF")),
|
||||
assert(Parse(L"HB4@0")),
|
||||
assert(Parse(L"/RS65001#@44")),
|
||||
assert(!Parse(L"HQ@4")),
|
||||
assert(Parse(L"HQ@4")),
|
||||
assert(!Parse(L"/RW@44")),
|
||||
assert(!Parse(L"/HWG@33"))
|
||||
);
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace HookCode
|
@ -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;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include "textthread.h"
|
||||
|
||||
namespace Host
|
@ -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,15 +42,28 @@ 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));
|
||||
else if (auto converted = StringToWideString(std::string((char*)data, length), hp.codepage ? hp.codepage : Host::defaultCodepage)) buffer.append(converted.value());
|
||||
else Host::AddConsoleOutput(INVALID_CODEPAGE);
|
||||
if (hp.type & FULL_STRING) buffer.push_back(L'\n');
|
||||
lastPushTime = GetTickCount();
|
||||
lastPushTime = GetTickCount64();
|
||||
|
||||
if (filterRepetition)
|
||||
{
|
||||
@ -70,6 +83,14 @@ void TextThread::Push(BYTE* data, int length)
|
||||
}
|
||||
}
|
||||
|
||||
void TextThread::Push(const wchar_t* data)
|
||||
{
|
||||
std::scoped_lock lock(bufferMutex);
|
||||
// not sure if this should filter repetition
|
||||
lastPushTime = GetTickCount64();
|
||||
buffer += data;
|
||||
}
|
||||
|
||||
void TextThread::Flush()
|
||||
{
|
||||
{
|
||||
@ -83,13 +104,13 @@ void TextThread::Flush()
|
||||
for (auto& sentence : sentences)
|
||||
{
|
||||
totalSize += sentence.size();
|
||||
sentence.erase(std::remove(sentence.begin(), sentence.end(), L'\0'), sentence.end());
|
||||
sentence.erase(std::remove(sentence.begin(), sentence.end(), 0), sentence.end());
|
||||
if (Output(*this, sentence)) storage->append(sentence);
|
||||
}
|
||||
|
||||
std::scoped_lock lock(bufferMutex);
|
||||
if (buffer.empty()) return;
|
||||
if (buffer.size() > maxBufferSize || GetTickCount() - lastPushTime > flushDelay)
|
||||
if (buffer.size() > maxBufferSize || GetTickCount64() - lastPushTime > flushDelay)
|
||||
{
|
||||
AddSentence(std::move(buffer));
|
||||
buffer.clear();
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include "types.h"
|
||||
|
||||
class TextThread
|
||||
@ -9,9 +8,9 @@ public:
|
||||
using OutputCallback = bool(*)(TextThread&, std::wstring&);
|
||||
inline static OutputCallback Output;
|
||||
|
||||
inline static bool filterRepetition = true;
|
||||
inline static int flushDelay = 400; // flush every 400ms by default
|
||||
inline static int maxBufferSize = 1000;
|
||||
inline static bool filterRepetition = false;
|
||||
inline static int flushDelay = 500; // flush every 500ms by default
|
||||
inline static int maxBufferSize = 3000;
|
||||
inline static int maxHistorySize = 10'000'000;
|
||||
|
||||
TextThread(ThreadParam tp, HookParam hp, std::optional<std::wstring> name = {});
|
||||
@ -20,6 +19,7 @@ public:
|
||||
void Stop();
|
||||
void AddSentence(std::wstring sentence);
|
||||
void Push(BYTE* data, int length);
|
||||
void Push(const wchar_t* data);
|
||||
|
||||
Synchronized<std::wstring> storage;
|
||||
const int64_t handle;
|
||||
@ -36,7 +36,7 @@ private:
|
||||
BYTE leadByte = 0;
|
||||
std::unordered_set<wchar_t> repeatingChars;
|
||||
std::mutex bufferMutex;
|
||||
DWORD lastPushTime = 0;
|
||||
DWORD64 lastPushTime = 0;
|
||||
Synchronized<std::vector<std::wstring>> queuedSentences;
|
||||
struct TimerDeleter { void operator()(HANDLE h) { DeleteTimerQueueTimer(NULL, h, INVALID_HANDLE_VALUE); } };
|
||||
AutoHandle<TimerDeleter> timer = NULL;
|
@ -2,6 +2,7 @@
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <concrt.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
@ -15,7 +16,6 @@
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <cstdint>
|
||||
@ -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,37 @@ 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
|
||||
template <typename F>
|
||||
void SpawnThread(const F& f) // works in DllMain unlike std thread
|
||||
{
|
||||
BYTE DUMMY[100];
|
||||
template <typename T>
|
||||
operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
|
||||
F* copy = new F(f);
|
||||
CloseHandle(CreateThread(nullptr, 0, [](void* copy)
|
||||
{
|
||||
(*(F*)copy)();
|
||||
delete (F*)copy;
|
||||
return 0UL;
|
||||
}, copy, 0, nullptr));
|
||||
}
|
||||
|
||||
inline struct
|
||||
{
|
||||
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; }
|
||||
inline auto Swallow = [](auto&&...) {};
|
||||
|
||||
template <typename C>
|
||||
inline auto FormatArg(const std::basic_string<C>& arg) { return arg.c_str(); }
|
||||
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; }
|
||||
template <typename C> inline auto FormatArg(const std::basic_string<C>& arg) { return arg.c_str(); }
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4996)
|
||||
@ -120,6 +120,12 @@ inline std::wstring FormatString(const wchar_t* format, const Args&... args)
|
||||
}
|
||||
#pragma warning(pop)
|
||||
|
||||
inline void Trim(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());
|
||||
}
|
||||
|
||||
inline std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding)
|
||||
{
|
||||
std::vector<wchar_t> buffer(text.size() + 1);
|
||||
@ -145,8 +151,13 @@ inline std::string WideStringToString(const std::wstring& text)
|
||||
template <typename... Args>
|
||||
inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { MessageBoxW(NULL, FormatString(format, args...).c_str(), L"Textractor", MB_OK); }
|
||||
|
||||
template <typename... Args>
|
||||
inline void TEXTRACTOR_DEBUG(const wchar_t* format, const Args&... args) { SpawnThread([=] { TEXTRACTOR_MESSAGE(format, args...); }); }
|
||||
|
||||
void Localize();
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr);
|
||||
#define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr)
|
||||
#else
|
||||
#define TEST(...)
|
||||
#endif
|
||||
|
@ -1,4 +1,3 @@
|
||||
#include "common.h"
|
||||
#include <Psapi.h>
|
||||
|
||||
inline std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL)
|
||||
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QHash>
|
||||
@ -10,6 +9,7 @@
|
||||
#include <QSettings>
|
||||
#include <QMainWindow>
|
||||
#include <QDialog>
|
||||
#include <QApplication>
|
||||
#include <QLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
@ -24,12 +24,13 @@ 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); } };
|
||||
struct Localizer { Localizer() { Localize(); } };
|
||||
inline std::wstring S(const QString& s) { return { s.toStdWString() }; }
|
||||
inline QString S(const std::string& s) { return QString::fromStdString(s); }
|
||||
inline QString S(const std::wstring& s) { return QString::fromStdWString(s); }
|
||||
// TODO: allow paired surrogates
|
||||
inline void sanitize(QString& s) { s.chop(std::distance(std::remove_if(s.begin(), s.end(), [](QChar ch) { return ch.isSurrogate(); }), s.end())); }
|
||||
inline QString sanitize(QString&& s) { QString result; for (auto ch : s) if (!ch.isSurrogate()) result.push_back(ch); return result; }
|
||||
inline QString sanitize(QString&& s) { sanitize(s); return std::move(s); }
|
||||
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include "const.h"
|
||||
|
||||
class WinMutex // Like CMutex but works with scoped_lock
|
||||
@ -63,7 +62,7 @@ struct SearchParam
|
||||
BYTE pattern[PATTERN_SIZE] = { x64 ? 0xcc : 0x55, x64 ? 0xcc : 0x8b, x64 ? 0x48 : 0xec, 0x89 }; // pattern in memory to search for
|
||||
int length = x64 ? 4 : 3, // length of pattern (zero means this SearchParam is invalid and the default should be used)
|
||||
offset = x64 ? 2 : 0, // offset from start of pattern to add hook
|
||||
searchTime = 20000, // ms
|
||||
searchTime = 30000, // ms
|
||||
maxRecords = 100000,
|
||||
codepage = SHIFT_JIS;
|
||||
uintptr_t padding = 0, // same as hook param padding
|
||||
|
@ -10,7 +10,8 @@ DefaultGroupName=Textractor
|
||||
MinVersion=6.1
|
||||
OutputBaseFilename=Textractor-{#VERSION}-Setup
|
||||
OutputDir=Builds
|
||||
PrivilegesRequired=lowest
|
||||
PrivilegesRequired=admin
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
SolidCompression=yes
|
||||
Uninstallable=no
|
||||
|
||||
@ -25,6 +26,7 @@ Name: "pt"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
|
||||
Name: "th"; MessagesFile: "compiler:Languages\Unofficial\Thai.isl"
|
||||
Name: "ko"; MessagesFile: "compiler:Languages\Unofficial\Korean.isl"
|
||||
Name: "it"; MessagesFile: "compiler:Languages\Italian.isl"
|
||||
Name: "fr"; MessagesFile: "compiler:Languages\French.isl"
|
||||
|
||||
|
||||
[Files]
|
||||
@ -39,3 +41,5 @@ Source: "Builds\Textractor-Portuguese-{#VERSION}\*"; DestDir: "{app}"; Languages
|
||||
Source: "Builds\Textractor-Thai-{#VERSION}\*"; DestDir: "{app}"; Languages: th; Flags: recursesubdirs ignoreversion
|
||||
Source: "Builds\Textractor-Korean-{#VERSION}\*"; DestDir: "{app}"; Languages: ko; Flags: recursesubdirs ignoreversion
|
||||
Source: "Builds\Textractor-Italian-{#VERSION}\*"; DestDir: "{app}"; Languages: it; Flags: recursesubdirs ignoreversion
|
||||
Source: "Builds\Textractor-French-{#VERSION}\*"; DestDir: "{app}"; Languages: fr; Flags: recursesubdirs ignoreversion
|
||||
Source: "INSTALL_THIS_UNICODE_FONT.ttf"; DestDir: "{autofonts}"; DestName: "ARIAL_UNICODE_MS.ttf"; FontInstall: "Arial Unicode MS";
|
||||
|
@ -4,4 +4,6 @@ find_qt5(Core Widgets)
|
||||
|
||||
add_executable(Test WIN32 main.cpp resource.rc)
|
||||
|
||||
target_precompile_headers(Test REUSE_FROM pch)
|
||||
|
||||
target_link_libraries(Test Qt5::Widgets)
|
||||
|
@ -1,5 +1,4 @@
|
||||
#include "common.h"
|
||||
#include "resource.h"
|
||||
#include "resource.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <QApplication>
|
||||
|
612
text.cpp
612
text.cpp
@ -14,12 +14,15 @@
|
||||
#define THAI
|
||||
#define PORTUGUESE
|
||||
#define KOREAN
|
||||
#define FRENCH
|
||||
#endif
|
||||
|
||||
// If you are updating a previous translation see https://github.com/Artikash/Textractor/issues/313
|
||||
|
||||
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";
|
||||
@ -30,8 +33,8 @@ const char* SETTINGS = u8"Settings";
|
||||
const char* EXTENSIONS = u8"Extensions";
|
||||
const char* SELECT_PROCESS = u8"Select process";
|
||||
const char* ATTACH_INFO = u8R"(If you don't see the process you want to attach, try running with admin rights
|
||||
You can also type in the process id)";
|
||||
const char* SELECT_PROCESS_INFO = u8"If you manually type in the process file name, please use the absolute path";
|
||||
You can also type in the process ID)";
|
||||
const char* SELECT_PROCESS_INFO = u8"If you manually type in the process file name, use the absolute path";
|
||||
const char* FROM_COMPUTER = u8"Select from computer";
|
||||
const char* PROCESSES = u8"Processes (*.exe)";
|
||||
const char* CODE_INFODUMP = u8R"(Enter read code
|
||||
@ -54,17 +57,18 @@ Negatives for data_offset/split_offset refer to registers
|
||||
-C for RAX, -14 for RBX, -1C for RCX, -24 for RDX, and so on for RSP, RBP, RSI, RDI, R8-R15
|
||||
* means dereference pointer+deref_offset)";
|
||||
const char* SAVE_SETTINGS = u8"Save settings";
|
||||
const char* EXTEN_WINDOW_INSTRUCTIONS = u8R"(To add an extension, right click the extension list
|
||||
Alternatively, drag and drop the extension file from your computer
|
||||
To reorder extensions, drag and drop them within the list
|
||||
(Extensions are used from top to bottom: order DOES matter)
|
||||
To remove an extension, select it and press delete)";
|
||||
const char* EXTEN_WINDOW_INSTRUCTIONS = u8R"(Right click the list to add or remove extensions
|
||||
Drag and drop extensions within the list to reorder them
|
||||
(Extensions are used from top to bottom: order DOES matter))";
|
||||
const char* ADD_EXTENSION = u8"Add extension";
|
||||
const char* REMOVE_EXTENSION = u8"Remove extension";
|
||||
const char* INVALID_EXTENSION = u8"%1 is an invalid extension";
|
||||
const char* CONFIRM_EXTENSION_OVERWRITE = u8"Another version of this extension already exists, do you want to delete and overwrite it?";
|
||||
const char* EXTENSION_WRITE_ERROR = u8"Failed to save extension";
|
||||
const char* USE_JP_LOCALE = u8"Emulate japanese locale?";
|
||||
const char* FAILED_TO_CREATE_CONFIG_FILE = u8"Failed to create config file \"%1\"";
|
||||
const char* HOOK_SEARCH_UNSTABLE_WARNING = u8"Searching for hooks is unstable! Be prepared for your game to crash!";
|
||||
const char* HOOK_SEARCH_STARTING_VIEW_CONSOLE = u8"Initializing hook search - please check console for further instructions";
|
||||
const char* SEARCH_CJK = u8"Search for Chinese/Japanese/Korean";
|
||||
const char* SEARCH_PATTERN = u8"Search pattern (hex byte array)";
|
||||
const char* SEARCH_DURATION = u8"Search duration (ms)";
|
||||
@ -93,16 +97,16 @@ const char* MAX_HISTORY_SIZE = u8"Max history size";
|
||||
const char* CONFIG_JP_LOCALE = u8"Launch with JP locale";
|
||||
const wchar_t* CONSOLE = L"Console";
|
||||
const wchar_t* CLIPBOARD = L"Clipboard";
|
||||
const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by me: Artikash (email: akashmozumdar@gmail.com)
|
||||
const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by Artikash (email: akashmozumdar@gmail.com)
|
||||
Project homepage: https://github.com/Artikash/Textractor
|
||||
Tutorial video: https://tinyurl.com/textractor-tutorial
|
||||
Tutorial video: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
FAQ: https://github.com/Artikash/Textractor/wiki/FAQ
|
||||
Please contact me with any problems, feature requests, or questions relating to Textractor
|
||||
Please contact Artikash with any problems, feature requests, or questions relating to Textractor
|
||||
You can do so via the project homepage (issues section) or via email
|
||||
Source code available under GPLv3 at project homepage
|
||||
If you like this project, please tell everyone about it :))";
|
||||
const wchar_t* CL_OPTIONS = LR"(usage: Textractor [-p{process id|"process name"}]...
|
||||
example: Textractor -p4466 -p"My Game.exe" tries to inject processes with id 4466 or with name My Game.exe)";
|
||||
If you like this project, please tell everyone about it! It's time to put AGTH down :))";
|
||||
const wchar_t* CL_OPTIONS = LR"(usage: Textractor [-p{process ID|"process name"}]...
|
||||
example: Textractor -p4466 -p"My Game.exe" tries to inject processes with ID 4466 or with name My Game.exe)";
|
||||
const wchar_t* UPDATE_AVAILABLE = L"Update available: download it from https://github.com/Artikash/Textractor/releases";
|
||||
const wchar_t* ALREADY_INJECTED = L"Textractor: already injected";
|
||||
const wchar_t* NEED_32_BIT = L"Textractor: architecture mismatch: only Textractor x86 can inject this process";
|
||||
@ -116,9 +120,10 @@ const char* INSERTING_HOOK = u8"Textractor: inserting hook: %s";
|
||||
const char* REMOVING_HOOK = u8"Textractor: removing hook: %s";
|
||||
const char* HOOK_FAILED = u8"Textractor: failed to insert hook";
|
||||
const char* TOO_MANY_HOOKS = u8"Textractor: too many hooks: can't insert";
|
||||
const char* STARTING_SEARCH = u8"Textractor: starting search";
|
||||
const char* HOOK_SEARCH_STARTING = u8"Textractor: starting hook search";
|
||||
const char* HOOK_SEARCH_INITIALIZING = u8"Textractor: initializing hook search (%f%%)";
|
||||
const char* NOT_ENOUGH_TEXT = u8"Textractor: not enough text to search accurately";
|
||||
const char* HOOK_SEARCH_INITIALIZED = u8"Textractor: search initialized with %zd hooks";
|
||||
const char* HOOK_SEARCH_INITIALIZED = u8"Textractor: initialized hook search with %zd hooks";
|
||||
const char* MAKE_GAME_PROCESS_TEXT = u8"Textractor: please click around in the game to force it to process text during the next %d seconds";
|
||||
const char* HOOK_SEARCH_FINISHED = u8"Textractor: hook search finished, %d results found";
|
||||
const char* OUT_OF_RECORDS_RETRY = u8"Textractor: out of search records, please retry if results are poor (default record count increased)";
|
||||
@ -130,19 +135,28 @@ const char* READ_ERROR = u8"Textractor: Reader ERROR (likely an incorrect R-code
|
||||
const char* HIJACK_ERROR = u8"Textractor: Hijack ERROR";
|
||||
const char* COULD_NOT_FIND = u8"Textractor: could not find text";
|
||||
const char* TRANSLATE_TO = u8"Translate to";
|
||||
const char* TRANSLATE_FROM = u8"Translate from";
|
||||
const char* FILTER_GARBAGE = u8"Filter garbage characters";
|
||||
const char* TRANSLATE_SELECTED_THREAD_ONLY = u8"Translate selected text thread only";
|
||||
const char* RATE_LIMIT_ALL_THREADS = u8"Rate limit all text threads";
|
||||
const char* RATE_LIMIT_ALL_THREADS = u8"Use rate limiter";
|
||||
const char* RATE_LIMIT_SELECTED_THREAD = u8"Rate limit selected text thread";
|
||||
const char* USE_TRANS_CACHE = u8"Use translation cache";
|
||||
const char* RATE_LIMIT_TOKEN_COUNT = u8"Rate limiter token count";
|
||||
const char* RATE_LIMIT_TOKEN_RESTORE_DELAY = u8"Rate limiter token restore delay (ms)";
|
||||
const char* MAX_TRANSLATIONS_IN_TIMESPAN = u8"Max translation requests in timespan";
|
||||
const char* TIMESPAN = u8"Timespan (ms)";
|
||||
const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS = L"Sentence too large to translate";
|
||||
const wchar_t* TOO_MANY_TRANS_REQUESTS = L"Rate limit exceeded: refuse to make more translation requests";
|
||||
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 = u8"Google Chrome file location";
|
||||
const char* START_DEVTOOLS = u8"Start DevTools";
|
||||
const char* STOP_DEVTOOLS = u8"Stop DevTools";
|
||||
const char* HIDE_CHROME = u8"Hide Chrome window";
|
||||
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";
|
||||
const char* MAX_SENTENCE_SIZE = u8"Max sentence size";
|
||||
const char* TOPMOST = u8"Always on top";
|
||||
const char* DICTIONARY = u8"Dictionary";
|
||||
@ -164,7 +178,13 @@ This file must be encoded in UTF-8.)";
|
||||
const char* SHOW_ORIGINAL = u8"Original text";
|
||||
const char* SHOW_ORIGINAL_INFO = u8R"(Original text will not be shown
|
||||
Only works if this extension is used directly after a translation extension)";
|
||||
const char* ORIGINAL_AFTER_TRANSLATION = u8"Original text after translation";
|
||||
const char* SIZE_LOCK = u8"Size lock";
|
||||
const char* POSITION_LOCK = u8"Position lock";
|
||||
const char* CENTERED_TEXT = u8"Centered text";
|
||||
const char* AUTO_RESIZE_WINDOW_HEIGHT = u8"Auto resize window height";
|
||||
const char* CLICK_THROUGH = u8"Click through\tAlt+X";
|
||||
const char* HIDE_MOUSEOVER = u8"Hide while mouse on top";
|
||||
const char* OPACITY = u8"Opacity";
|
||||
const char* BG_COLOR = u8"Background color";
|
||||
const char* TEXT_COLOR = u8"Text color";
|
||||
@ -187,13 +207,13 @@ Modifications to global variables from ProcessSentence are not guaranteed to per
|
||||
|
||||
Properties in sentenceInfo:
|
||||
"current select": 0 unless sentence is in the text thread currently selected by the user.
|
||||
"process id": process id that the sentence is coming from. 0 for console and clipboard.
|
||||
"process id": process ID that the sentence is coming from. 0 for console and clipboard.
|
||||
"text number": number of the current text thread. Counts up one by one as text threads are created. 0 for console, 1 for clipboard.
|
||||
--]]
|
||||
function ProcessSentence(sentence, sentenceInfo)
|
||||
--Your code here...
|
||||
end)";
|
||||
const char* LOAD_LUA_SCRIPT = u8"Load script";
|
||||
const char* LOAD_SCRIPT = u8"Load script";
|
||||
const wchar_t* LUA_ERROR = L"Lua error: %s";
|
||||
const char* REGEX_FILTER = u8"Regex Filter";
|
||||
const char* INVALID_REGEX = u8"Invalid regex";
|
||||
@ -205,13 +225,25 @@ All text in this file outside of a replacement command is ignored.
|
||||
A caret (^) acts as a wildcard that matches any other single character.
|
||||
Whitespace in original_text is ignored, but replacement_text can contain spaces, newlines, etc.
|
||||
This file must be encoded in Unicode (UTF-16 Little Endian).)";
|
||||
const wchar_t* REGEX_REPLACER_INSTRUCTIONS = LR"(This file only does anything when the "Regex Replacer" extension is used.
|
||||
Replacement commands must be formatted like this:
|
||||
|REGEX|regular_expression|BECOMES|replacement_text|MODIFIER|modifiers|END|
|
||||
replacement_text can reference capture groups with a $ followed by their number (e.g. $1 references first capture group).
|
||||
modifiers can contain the following:
|
||||
"g" the replacement is global.
|
||||
"i" the replacement ignores the case.
|
||||
If empty the replacement is only for the first match and case sensitive.
|
||||
All text in this file outside of a replacement command is ignored.
|
||||
This file must be encoded in Unicode (UTF-16 Little Endian).
|
||||
Learn, build, & test Regular Expressions: https://regexr.com/)";
|
||||
const char* THREAD_LINKER = u8"Thread Linker";
|
||||
const char* LINK = u8"Link";
|
||||
const char* UNLINK = u8"Unlink";
|
||||
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";
|
||||
@ -273,7 +305,7 @@ Presiona supr en una extension seleccionada para removerla)";
|
||||
CLIPBOARD = L"Portapapeles";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( hecho por mí: Artikash (correo: akashmozumdar@gmail.com)
|
||||
Página del proyecto: https://github.com/Artikash/Textractor
|
||||
Video tutorial: https://tinyurl.com/textractor-tutorial
|
||||
Video tutorial: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
No dudes en conectarme si tienes algún problema, petición de característica o preguntas relacionadas con Textractor
|
||||
Puedes hacerlo en la página del proyecto (en el apartado de "Issues") o por correo. Usa el inglés para comunicarte.
|
||||
Código fuente disponible bajo GPLv3 en la página del proyecto)";
|
||||
@ -309,39 +341,75 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
#endif // SPANISH
|
||||
|
||||
#ifdef SIMPLIFIED_CHINESE
|
||||
NATIVE_LANGUAGE = "Chinese (simplified)";
|
||||
NATIVE_LANGUAGE = "Chinese (Simplified)";
|
||||
ATTACH = u8"附加到游戏";
|
||||
LAUNCH = u8"启动游戏";
|
||||
CONFIG = u8"配置游戏";
|
||||
DETACH = u8"从游戏分离";
|
||||
FORGET = u8"移除游戏";
|
||||
ADD_HOOK = u8"添加钩子";
|
||||
REMOVE_HOOKS = u8"移除钩子";
|
||||
SAVE_HOOKS = u8"保存钩子";
|
||||
SEARCH_FOR_HOOKS = u8"搜索钩子";
|
||||
SETTINGS = u8"设置";
|
||||
EXTENSIONS = u8"扩展";
|
||||
SELECT_PROCESS = u8"选择进程";
|
||||
ATTACH_INFO = u8R"(如果没看见想要附加的进程,尝试使用管理员权限运行
|
||||
ATTACH_INFO = u8R"(如果没看见想要附加的进程,请尝试使用管理员权限运行Textractor
|
||||
也可以手动输入进程ID)";
|
||||
FROM_COMPUTER = u8"从计算机中选择";
|
||||
PROCESSES = u8"进程 (*.exe)";
|
||||
SELECT_PROCESS_INFO = u8"如果手动输入游戏名,请使用绝对路径";
|
||||
FROM_COMPUTER = u8"从资源管理器中选择";
|
||||
PROCESSES = u8"可执行文件 (*.exe)";
|
||||
SAVE_SETTINGS = u8"保存设置";
|
||||
EXTEN_WINDOW_INSTRUCTIONS = u8R"(从计算机拖拽扩展 (.dll) 文件到这里来添加
|
||||
(如果使用超级管理员运行,则无法工作)
|
||||
在列表中拖拽来重新排序
|
||||
使用 delete 键移除选中的扩展)";
|
||||
EXTEN_WINDOW_INSTRUCTIONS = u8R"(在列表中单击右键以添加或移除扩展
|
||||
在列表中拖拽扩展可以调整插件的执行顺序, 扩展会从上往下依次执行
|
||||
排在下方的扩展只能接收到由排在上方的扩展处理过后的文本)";
|
||||
ADD_EXTENSION = u8"添加扩展";
|
||||
REMOVE_EXTENSION = u8"移除扩展";
|
||||
INVALID_EXTENSION = u8"%1 是一个无效扩展";
|
||||
CONFIRM_EXTENSION_OVERWRITE = u8"此扩展的另一个版本已启用,是否删除并覆盖它?";
|
||||
EXTENSION_WRITE_ERROR = u8"保存扩展失败";
|
||||
USE_JP_LOCALE = u8"模拟日本区域设置?";
|
||||
FAILED_TO_CREATE_CONFIG_FILE = u8"无法创建配置文件 \"%1\"";
|
||||
HOOK_SEARCH_UNSTABLE_WARNING = u8"搜索钩子的功能是不稳定的! 可能会导致你的游戏崩溃! ";
|
||||
HOOK_SEARCH_STARTING_VIEW_CONSOLE = u8"正在初始化钩子搜索 - 请查看控制台以获取更多提示";
|
||||
SEARCH_CJK = u8"搜索中文/日文/韩文";
|
||||
SEARCH_PATTERN = u8"搜索匹配特征 (hex byte array)";
|
||||
SEARCH_DURATION = u8"搜索持续时间 (ms)";
|
||||
SEARCH_MODULE = u8"搜索指定模块";
|
||||
PATTERN_OFFSET = u8"相对于特征地址的偏移值";
|
||||
MAX_HOOK_SEARCH_RECORDS = u8"搜索结果达到上限";
|
||||
MIN_ADDRESS = u8"起始地址 (hex)";
|
||||
MAX_ADDRESS = u8"结束地址 (hex)";
|
||||
STRING_OFFSET = u8"字符串偏移值 (hex)";
|
||||
HOOK_SEARCH_FILTER = u8"结果必须匹配的正则表达式";
|
||||
TEXT = u8"文本";
|
||||
CODEPAGE = u8"代码页";
|
||||
SEARCH_FOR_TEXT = u8"搜索指定文本";
|
||||
START_HOOK_SEARCH = u8"开始搜索钩子";
|
||||
SAVE_SEARCH_RESULTS = u8"保存搜索结果";
|
||||
TEXT_FILES = u8"文本文档 (*.txt)";
|
||||
DOUBLE_CLICK_TO_REMOVE_HOOK = u8"双击移除不需要的钩子";
|
||||
FILTER_REPETITION = u8"过滤重复文本";
|
||||
AUTO_ATTACH = u8"自动附加";
|
||||
ATTACH_SAVED_ONLY = u8"自动附加 (仅限保存过配置的游戏)";
|
||||
SHOW_SYSTEM_PROCESSES = u8"显示系统进程";
|
||||
DEFAULT_CODEPAGE = u8"默认代码页";
|
||||
FLUSH_DELAY = u8"刷新延迟";
|
||||
MAX_BUFFER_SIZE = u8"最大缓冲区长度";
|
||||
MAX_HISTORY_SIZE = u8"最大缓存文本长度";
|
||||
CONFIG_JP_LOCALE = u8"模拟日本区域启动游戏";
|
||||
CONSOLE = L"控制台";
|
||||
CLIPBOARD = L"剪贴板";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( 作者: Artikash (email: akashmozumdar@gmail.com)
|
||||
项目主页: https://github.com/Artikash/Textractor
|
||||
教程视频: https://tinyurl.com/textractor-tutorial
|
||||
教程视频: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
如果有任何关于 Textractor 的困难,功能请求或问题,请联系我
|
||||
可以通过项目主页 (问题区) 或通过邮件来联系
|
||||
可以通过项目主页 (Issues 页面) 或通过邮件来联系
|
||||
项目主页提供基于 GPLv3 协议的源代码)";
|
||||
UPDATE_AVAILABLE = L"有可用的更新: 请从 https://github.com/Artikash/Textractor/releases 下载";
|
||||
ALREADY_INJECTED = L"Textractor: 已经注入";
|
||||
NEED_32_BIT = L"Textractor: 架构不匹配: 请尝试使用 Textractor x86";
|
||||
NEED_32_BIT = L"Textractor: 架构不匹配: 请尝试使用32位版本的Textractor";
|
||||
NEED_64_BIT = L"Textractor: 架构不匹配: 请尝试使用64位版本的Textractor";
|
||||
INJECT_FAILED = L"Textractor: 无法注入";
|
||||
LAUNCH_FAILED = L"Textractor: 无法启动";
|
||||
INVALID_CODE = L"Textractor: 无效代码";
|
||||
@ -350,8 +418,14 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
INSERTING_HOOK = u8"Textractor: 注入钩子: %s";
|
||||
REMOVING_HOOK = u8"Textractor: 移除钩子: %s";
|
||||
HOOK_FAILED = u8"Textractor: 钩子注入失败";
|
||||
TOO_MANY_HOOKS = u8"Textractor: 钩子太多: 无法注入";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: 没有足够的文本来精确搜索";
|
||||
TOO_MANY_HOOKS = u8"Textractor: 钩子数量已达上限: 无法注入";
|
||||
HOOK_SEARCH_STARTING = u8"Textractor: 开始搜索钩子";
|
||||
HOOK_SEARCH_INITIALIZING = u8"Textractor: 初始化钩子搜索 (%f%%)";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: 文本长度不足, 无法精确搜索";
|
||||
HOOK_SEARCH_INITIALIZED = u8"Textractor: 搜索初始化完成, 创建了 %zd 个钩子";
|
||||
MAKE_GAME_PROCESS_TEXT = u8"Textractor: 请点击游戏区域, 在接下来的 %d 秒内使游戏强制处理文本";
|
||||
HOOK_SEARCH_FINISHED = u8"Textractor: 钩子搜索完毕, 找到了 %d 条结果";
|
||||
OUT_OF_RECORDS_RETRY = u8"Textractor: 搜索结果已达上限, 如果结果不理想, 请重试(默认最大记录数增加)";
|
||||
FUNC_MISSING = u8"Textractor: 函数不存在";
|
||||
MODULE_MISSING = u8"Textractor: 模块不存在";
|
||||
GARBAGE_MEMORY = u8"Textractor: 内存一直在变,读了也没用";
|
||||
@ -359,22 +433,107 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
READ_ERROR = u8"Textractor: Reader 错误 (R码可能不正确)";
|
||||
HIJACK_ERROR = u8"Textractor: Hijack 错误";
|
||||
COULD_NOT_FIND = u8"Textractor: 无法找到文本";
|
||||
TOO_MANY_TRANS_REQUESTS = L"太多翻译请求: 拒绝生成更多";
|
||||
TRANSLATE_TO = u8"翻译为";
|
||||
TRANSLATE_FROM = u8"原文语言";
|
||||
FILTER_GARBAGE = u8"过滤无意义字符";
|
||||
TRANSLATE_SELECTED_THREAD_ONLY = u8"仅翻译当前选择的文本线程";
|
||||
RATE_LIMIT_ALL_THREADS = u8"限制请求频率";
|
||||
RATE_LIMIT_SELECTED_THREAD = u8"限制选中线程的请求频率";
|
||||
USE_TRANS_CACHE = u8"使用缓存的翻译结果";
|
||||
MAX_TRANSLATIONS_IN_TIMESPAN = u8"单位时间内最大请求次数";
|
||||
TIMESPAN = u8"单位时间长度 (ms)";
|
||||
TOO_MANY_TRANS_REQUESTS = L"超出频率限制: 拒绝发出翻译请求";
|
||||
TRANSLATION_ERROR = L"翻译时出错";
|
||||
EXTRA_WINDOW_INFO = u8R"(右键修改设置
|
||||
在窗口边缘点击并拖拽来移动,或在右下角点击并拖拽来调整大小)";
|
||||
USE_PREV_SENTENCE_CONTEXT = u8"使用之前的句子作为上下文";
|
||||
API_KEY = u8"API key";
|
||||
CHROME_LOCATION = u8"Google Chrome 安装位置";
|
||||
START_DEVTOOLS = u8"启动 DevTools";
|
||||
STOP_DEVTOOLS = u8"停止 DevTools";
|
||||
HIDE_CHROME = u8"隐藏 Chrome 窗口";
|
||||
DEVTOOLS_STATUS = u8"DevTools 状态";
|
||||
AUTO_START = u8"自动启动";
|
||||
ERROR_START_CHROME = L"无法启动或连接到 Chrome";
|
||||
EXTRA_WINDOW_INFO = u8R"(在此点击右键以修改设置
|
||||
在窗口边缘点击并拖拽来移动, 在窗口右下角点击并拖拽来调整大小)";
|
||||
MAX_SENTENCE_SIZE = u8"最大文本长度";
|
||||
TOPMOST = u8"窗口总是置顶";
|
||||
DICTIONARY = u8"字典";
|
||||
SHOW_ORIGINAL = u8"显示原文";
|
||||
SHOW_ORIGINAL_INFO = u8R"(原文将被隐藏
|
||||
仅当此扩展位于翻译扩展之后使用时才有效)";
|
||||
ORIGINAL_AFTER_TRANSLATION = u8"把原文显示在翻译文本的后面";
|
||||
SIZE_LOCK = u8"锁定窗口大小";
|
||||
POSITION_LOCK = u8"锁定窗口位置";
|
||||
CENTERED_TEXT = u8"居中显示文本";
|
||||
AUTO_RESIZE_WINDOW_HEIGHT = u8"自动改变窗口高度";
|
||||
CLICK_THROUGH = u8"点击穿透\tAlt+X";
|
||||
HIDE_MOUSEOVER = u8"鼠标经过时隐藏窗口";
|
||||
OPACITY = u8"透明度";
|
||||
BG_COLOR = u8"背景颜色";
|
||||
TEXT_COLOR = u8"文本颜色";
|
||||
TOPMOST = u8"总是位于最上层";
|
||||
TEXT_OUTLINE = u8"文字描边";
|
||||
OUTLINE_COLOR = u8"文字描边颜色";
|
||||
OUTLINE_SIZE = u8"文字描边大小";
|
||||
OUTLINE_SIZE_INFO = u8"描边的单位为px (建议保持在字体大小的20%以下)";
|
||||
FONT = u8"字体";
|
||||
LUA_INTRO = u8R"(--[[
|
||||
ProcessSentence 是Textractor每接收到一句文本时都会调用的函数.
|
||||
参数 sentence: Textractor接收到的文本 (UTF-8), 数据类型为 string.
|
||||
参数 sentenceInfo: 用于保存文本相关信息的表, 数据类型为 tabel.
|
||||
|
||||
如果你返回一个字符串,文本将被转换为该字符串.
|
||||
如果返回 nil, 该文本将不会被修改.
|
||||
|
||||
此扩展使用了几个Lua解释器的副本用于保证线程安全.
|
||||
在 ProcessSentence 函数中对全局变量的修改可能不会生效.
|
||||
|
||||
sentenceInfo 表有以下关键字索引:
|
||||
current select: 除非文本属于用户当前选择的文本线程, 否则为0. 数据类型为 number.
|
||||
process id: 这句文本所属的进程ID. 0 表示控制台与剪贴板线程. 数据类型为 number.
|
||||
text number: 当前选择的文本线程的ID. 这是在创建文本线程时自动递增的计数器. 0 为控制台, 1 为剪贴板. 数据类型为 number.
|
||||
--]]
|
||||
|
||||
function ProcessSentence(sentence, sentenceInfo)
|
||||
--在此处添加你的代码...
|
||||
end)";
|
||||
LOAD_SCRIPT = u8"加载脚本";
|
||||
LUA_ERROR = L"Lua 错误";
|
||||
REGEX_FILTER = u8"正则表达式过滤器";
|
||||
INVALID_REGEX = u8"无效的正则表达式";
|
||||
CURRENT_FILTER = u8"当前过滤中: %1";
|
||||
REPLACER_INSTRUCTIONS = LR"(使用"Replace"扩展时会使用此文件
|
||||
替换指令必须遵循以下格式:
|
||||
|ORIG|原文|BECOMES|替代文本|END|
|
||||
此文件中替换指令之外的所有文本都会被忽略.
|
||||
符号 ^ 可以代替任意一个字符.
|
||||
原文中的空白字符将被忽略,但是替代文本可以包含空白字符、换行符等.
|
||||
此文件必须使用 Unicode 编码 (UTF-16 little endian).)";
|
||||
REGEX_REPLACER_INSTRUCTIONS = LR"(使用"Regex Replace"扩展时会使用此文件
|
||||
替换指令必须遵循以下格式:
|
||||
|REGEX|正则表达式|BECOMES|替代文本|MODIFIER|修饰符|END|
|
||||
替代文本可以通过 $[编号] 来引用捕获分组 (例如: $1 引用第1个捕获分组).
|
||||
修饰符可以包含以下选项:
|
||||
g 全局替换.
|
||||
i 忽略大小写差异.
|
||||
如果替代文本为空, 那么只会替换第1个匹配的到结果, 而且强制启用大小写敏感.
|
||||
此文件中替换指令之外的所有文本都会被忽略.
|
||||
此文件必须使用 Unicode 编码 (UTF-16 little endian).
|
||||
你可以通过下列链接来学习编写和在线测试正则表达式:
|
||||
https://www.runoob.com/regexp/regexp-intro.html
|
||||
https://regexr.com/)";
|
||||
THREAD_LINKER = u8"线程链接器";
|
||||
LINK = u8"链接";
|
||||
UNLINK = u8"断开";
|
||||
THREAD_LINK_FROM = u8"需要链接的线程ID";
|
||||
THREAD_LINK_TO = u8"链接到的线程ID";
|
||||
HEXADECIMAL = u8"十六进制";
|
||||
#endif // SIMPLIFIED_CHINESE
|
||||
|
||||
#ifdef RUSSIAN
|
||||
NATIVE_LANGUAGE = "Russian";
|
||||
ATTACH = u8"Присоединить к игре";
|
||||
LAUNCH = u8"Запустить игру";
|
||||
CONFIG = u8"Настройки игры";
|
||||
DETACH = u8"Отсоединить от игры";
|
||||
FORGET = u8"Забыть игру";
|
||||
ADD_HOOK = u8"Добавить хук";
|
||||
@ -385,28 +544,54 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
EXTENSIONS = u8"Расширения";
|
||||
SELECT_PROCESS = u8"Выберете процесс";
|
||||
ATTACH_INFO = u8R"(Если вы не видите процесс, к которому хотите присоединить, попробуйте запуск с правами администратора
|
||||
Вы также можете ввести id процесса)";
|
||||
Вы также можете ввести ID процесса)";
|
||||
SELECT_PROCESS_INFO = u8"При ручном вводе имени файла процесса используйте абсолютный путь";
|
||||
FROM_COMPUTER = u8"Найти в проводнике";
|
||||
PROCESSES = u8"Процессы (*.exe)";
|
||||
CODE_INFODUMP = u8R"(Введите код чтения
|
||||
R{S|Q|V|M}[null_length<][codepage#]@addr
|
||||
ИЛИ
|
||||
Введите хук-код
|
||||
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]]
|
||||
Все цифры кроме codepage/null_length в hexadecimal
|
||||
Кодировка по умолчанию - 932 (Shift-JIS), но может быть изменена в настройках
|
||||
A/B: символ кодировки little/big endian
|
||||
W: символ UTF-16
|
||||
H: Два hex байта
|
||||
S/Q/V/M: codepage/UTF-16/UTF-8/hex string
|
||||
F: treat strings as full lines of text
|
||||
N: не использовать контекст
|
||||
null_length: длина null прерывателя, используемого в строке
|
||||
padding: длина добавочных данных перед строкой (C struct { int64_t size; char string[500]; } needs padding = 8)
|
||||
Отрицательное для data_offset/split_offset ссылается на регистры
|
||||
-4 для EAX, -8 для ECX, -C для EDX, -10 для EBX, -14 для ESP, -18 для EBP, -1C для ESI, -20 для EDI
|
||||
-C для RAX, -14 для RBX, -1C для RCX, -24 для RDX, и так далее для RSP, RBP, RSI, RDI, R8-R15
|
||||
* значит указатель разницы+deref_offset)";
|
||||
SAVE_SETTINGS = u8"Сохранить настройки";
|
||||
EXTEN_WINDOW_INSTRUCTIONS = u8R"(Перетащите сюда (.dll) файлы расширений из проводника для их добавления
|
||||
(Не работает при запуске от администратора)
|
||||
Перетаскивайте по списку для изменения порядка
|
||||
Нажмите клавишу удаления, чтобы удалить выбранное расширение)";
|
||||
ADD_EXTENSION = u8"Добавить расширение";
|
||||
INVALID_EXTENSION = u8"%1 - неверное расширение";
|
||||
CONFIRM_EXTENSION_OVERWRITE = u8"Уже существует другая версия этого расширения, перезаписать его?";
|
||||
EXTENSION_WRITE_ERROR = u8"Не удалось сохранить расширение";
|
||||
USE_JP_LOCALE = u8"Симулировать японскую локаль?";
|
||||
FAILED_TO_CREATE_CONFIG_FILE = u8"Не удалось создать файл настроек \"%1\"";
|
||||
HOOK_SEARCH_UNSTABLE_WARNING = u8"Поиск хуков нестабилен! Игра может закрыться с ошибкой!";
|
||||
SEARCH_CJK = u8"Поиск для Китайского/Японского/Корейского";
|
||||
SEARCH_PATTERN = u8"Шаблон поиска (hex byte array)";
|
||||
SEARCH_DURATION = u8"Продолжительность поиска (ms)";
|
||||
SEARCH_MODULE = u8"Поиск по модулю";
|
||||
PATTERN_OFFSET = u8"Смещение от начала шаблона";
|
||||
MAX_HOOK_SEARCH_RECORDS = u8"Порог поиска";
|
||||
MIN_ADDRESS = u8"Начальный адрес (hex)";
|
||||
MAX_ADDRESS = u8"Конечный адрес (hex)";
|
||||
STRING_OFFSET = u8"Смещение строки (hex)";
|
||||
HOOK_SEARCH_FILTER = u8"Результат должен совпадать с этим regex";
|
||||
TEXT = u8"Текст";
|
||||
CODEPAGE = u8"Кодировка";
|
||||
SEARCH_FOR_TEXT = u8"Поиск определенного текста";
|
||||
START_HOOK_SEARCH = u8"Начать поиск хуков";
|
||||
SAVE_SEARCH_RESULTS = u8"Сохранить результат поиска";
|
||||
TEXT_FILES = u8"Текст (*.txt)";
|
||||
@ -414,20 +599,23 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
FILTER_REPETITION = u8"Фильтр повторений";
|
||||
AUTO_ATTACH = u8"Авто-присоединение";
|
||||
ATTACH_SAVED_ONLY = u8"Авто-присоединение (только сохраненные)";
|
||||
SHOW_SYSTEM_PROCESSES = u8"Показать системные процесы";
|
||||
DEFAULT_CODEPAGE = u8"Кодировка по умолчанию";
|
||||
FLUSH_DELAY = u8"Задержка сброса";
|
||||
MAX_BUFFER_SIZE = u8"Максимальный размер буфера";
|
||||
MAX_HISTORY_SIZE = u8"Макс. размер истории";
|
||||
CONFIG_JP_LOCALE = u8"Запуск с JP локалью";
|
||||
CONSOLE = L"Консоль";
|
||||
CLIPBOARD = L"Буфер обмена";
|
||||
ABOUT = L"Textractor " ARCH L" в." VERSION LR"( автор: Artikash (email: akashmozumdar@gmail.com)
|
||||
Домашняя страница: https://github.com/Artikash/Textractor
|
||||
Обучающее видео: https://tinyurl.com/textractor-tutorial
|
||||
Обучающее видео: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
Сообщайте о любых проблемах, желаемых для добавления функциях, или задавайте вопросы, касающиеся Textractor
|
||||
Сделать это вы можете на домашней странице (секция issues) или через электронную почту
|
||||
Исходный код доступен по лицензии GPLv3 на домашней странице проекта
|
||||
Если эта программа вам понравилась, расскажите всем о ней :))";
|
||||
CL_OPTIONS = LR"(использование: Textractor [-p{process id|"process name"}]...
|
||||
пример: Textractor -p4466 -p"My Game.exe" попробует присоединиться к процессу с id 4466 или с именем My Game.exe)";
|
||||
CL_OPTIONS = LR"(использование: Textractor [-p{process ID|"process name"}]...
|
||||
пример: Textractor -p4466 -p"My Game.exe" попробует присоединиться к процессу с ID 4466 или с именем My Game.exe)";
|
||||
UPDATE_AVAILABLE = L"Доступно обновление: загрузите его на https://github.com/Artikash/Textractor/releases";
|
||||
ALREADY_INJECTED = L"Textractor: уже присоединен";
|
||||
NEED_32_BIT = L"Textractor: несоответствие архитектуры: попробуйте Textractor x86 вместо этого";
|
||||
@ -441,28 +629,60 @@ Clic y arrastra los bordes de la ventana para moverla, o en la esquina inferior
|
||||
REMOVING_HOOK = u8"Textractor: удаление хука: %s";
|
||||
HOOK_FAILED = u8"Textractor: не удалось вставить хук";
|
||||
TOO_MANY_HOOKS = u8"Textractor: слишком много хуков: невозможно вставить";
|
||||
STARTING_SEARCH = u8"Textractor: начало поиска";
|
||||
HOOK_SEARCH_STARTING = u8"Textractor: начало поиска";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: не достаточно текста для точного поиска";
|
||||
HOOK_SEARCH_INITIALIZED = u8"Textractor: поиск инициализирован с %zd хуками";
|
||||
MAKE_GAME_PROCESS_TEXT = u8"Textractor: покликайте в игре, чтобы вызвать смену текста в течение %d секунд";
|
||||
HOOK_SEARCH_FINISHED = u8"Textractor: поиск хуков завершен, %d результатов найдено";
|
||||
OUT_OF_RECORDS_RETRY = u8"Textractor: записи для поиска закончились, повторите,если результаты неудовлетворительны (количество записей по умолчанию увеличено)";
|
||||
FUNC_MISSING = u8"Textractor: функция отсутствует";
|
||||
MODULE_MISSING = u8"Textractor: модуль отсутствует";
|
||||
GARBAGE_MEMORY = u8"Textractor: память постоянно изменяется, бесполезно читать";
|
||||
SEND_ERROR = u8"Textractor: Send ERROR (вероятно неверный H-code)";
|
||||
READ_ERROR = u8"Textractor: Reader ERROR (вероятно неверный R-code)";
|
||||
HIJACK_ERROR = u8"Textractor: Hijack ERROR";
|
||||
COULD_NOT_FIND = u8"Textractor: невозможно найти текст";
|
||||
TRANSLATE_TO = u8"Переводить на";
|
||||
TRANSLATE_SELECTED_THREAD_ONLY = u8"Переводить текст только в выбранном потоке";
|
||||
RATE_LIMIT_ALL_THREADS = u8"Ограничение скорости для всех текстовых потоков";
|
||||
RATE_LIMIT_SELECTED_THREAD = u8"Ограничение скорости выделенного текстового потока";
|
||||
USE_TRANS_CACHE = u8"Использовать кеш перевода";
|
||||
MAX_TRANSLATIONS_IN_TIMESPAN = u8"Ограничение частоты подсчёта токенов";
|
||||
TIMESPAN = u8"Ограничение частоты задержки восстановления токенов (мс)";
|
||||
TOO_MANY_TRANS_REQUESTS = L"Слишком много запросов для перевода: отклонено";
|
||||
TRANSLATION_ERROR = L"Ошибка при переводе";
|
||||
USE_PREV_SENTENCE_CONTEXT = u8"Использовать предыдущее предложение как контекст";
|
||||
API_KEY = u8"Ключ API";
|
||||
EXTRA_WINDOW_INFO = u8R"(Правый клик для изменения настроек
|
||||
Нажмите и перетащите за края - для перемещения, или за правый-нижний угол - для изменения размера)";
|
||||
MAX_SENTENCE_SIZE = u8"Максимальная длина предложения";
|
||||
TOPMOST = u8"Поверх всех окон";
|
||||
DICTIONARY = u8"Словарь";
|
||||
DICTIONARY_INSTRUCTIONS = u8R"(Этот файл использован только для функции "Словарь" расширения Extra Window.
|
||||
Он использует свой формат, специально для Textractor и не предназначен для рачного ввода.
|
||||
Смотрите онлайн словарь в этом формате(https://github.com/Artikash/Textractor-Dictionaries/releases - начните здесь).
|
||||
В качестве альтернативы, если вы программист, вы можете написать скрипт для конвертирования словаря из другого формата с информацией, приведенной ниже.
|
||||
При наличии словаря, для просмотра текста в Экстра окне просто наведите курсор на него. Можно прокрутить все совпадающие определения.
|
||||
Определения имеют следующий формат:|TERM|Hola<<ignored|TERM|hola|TERM|Bonjour|TERM|bonjour|DEFINITION|hello|END|
|
||||
Термин и определение могут включать форматированный текст(https://doc.qt.io/qt-5/richtext-html-subset.html), который будет соответствующе отформатирован.
|
||||
Перефразировки имеют следующий формат:|ROOT|1<<noun|INFLECTS TO|(\w*)s|NAME| plural|END|
|
||||
Textractor проверит, совпадает ли термин с регексом перефразировки и при совпадении будет рекурсивно искать корневой термин..
|
||||
Корневой термин генерируется путем замены каждого числа на соответствующую regex группу захвата(при этом 0 заменяется на полное совпадение).
|
||||
Этот процесс может легко привести к бесконечным циклам и/или переполнению стека. Ваша задача - избежать этого.
|
||||
regex перефразировок использует QRegularExpression (https://doc.qt.io/qt-5/qregularexpression.html) unicode синтаксис.
|
||||
Textractor отобразит конечный корневой термин, а также все перефразировки, используемые для получения этого корневого термина.
|
||||
Однако,текст в термине после << игнорируется при отображении. Это сделано для хранения информации о части речи.
|
||||
У этого файла кодировка должна быть UTF-8.)";
|
||||
SHOW_ORIGINAL = u8"Исходный текст";
|
||||
SHOW_ORIGINAL_INFO = u8R"(Исходный текст будет скрыт
|
||||
Работает только если это расширение используется после расширения перевода)";
|
||||
SIZE_LOCK = u8"Фиксированный размер";
|
||||
OPACITY = u8"Прозрачность";
|
||||
BG_COLOR = u8"Цвет заднего фона";
|
||||
TEXT_COLOR = u8"Цвет текста";
|
||||
TEXT_OUTLINE = u8"Обводка текста";
|
||||
OUTLINE_COLOR = u8"Цвет обводки";
|
||||
OUTLINE_SIZE = u8"Толщина обводки";
|
||||
OUTLINE_SIZE_INFO = u8"Размер в пикселях (рекомендуется оставить как 20% от размера шрифта)";
|
||||
FONT = u8"Шрифт";
|
||||
LUA_INTRO = u8R"(--[[
|
||||
ProcessSentence вызывается каждый раз, когда Textractor получает предложение с текстом.
|
||||
@ -478,13 +698,13 @@ Param sentenceInfo: таблица различной информации о п
|
||||
|
||||
Параметры в sentenceInfo:
|
||||
"current select": равно 0, если предложение не находится в текстовой нити, выбранной в данный момент пользователем.
|
||||
"process id": id процесса, из которого предложение поступило. Равно 0, когда это консоль или буфер обмена.
|
||||
"process id": ID процесса, из которого предложение поступило. Равно 0, когда это консоль или буфер обмена.
|
||||
"text number": номер текущей текстовой нити. Растет один за другим по мере создания текстовых нитей. 0 для консоли, 1 для буфера обмена.
|
||||
--]]
|
||||
function ProcessSentence(sentence, sentenceInfo)
|
||||
--Ваш код здесь...
|
||||
end)";
|
||||
LOAD_LUA_SCRIPT = u8"Загрузить скрипт";
|
||||
LOAD_SCRIPT = u8"Загрузить скрипт";
|
||||
LUA_ERROR = L"Ошибка Lua: %s";
|
||||
REGEX_FILTER = u8"Фильтр Regex";
|
||||
INVALID_REGEX = u8"Неверный regex";
|
||||
@ -512,7 +732,7 @@ end)";
|
||||
EXTENSIONS = u8"Ekstensi";
|
||||
SELECT_PROCESS = u8"Pilih Proses";
|
||||
ATTACH_INFO = u8R"(Jika kamu tidak dapat melihat proses yang akan ditempelkan, coba menjalankan dengan mode administrator
|
||||
Kamu juga dapat mengetik process id game yang akan ditempel)";
|
||||
Kamu juga dapat mengetik process ID game yang akan ditempel)";
|
||||
FROM_COMPUTER = u8"Pilih dari komputer";
|
||||
PROCESSES = u8"Proses (*.exe)";
|
||||
SAVE_SETTINGS = u8"Simpan pengaturan";
|
||||
@ -528,7 +748,7 @@ Tekan delete pada ekstensi yang dipilih untuk menghapus ekstensi)";
|
||||
CLIPBOARD = L"Papan clipboard";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( dibuat oleh saya: Artikash (email: akashmozumdar@gmail.com)
|
||||
Halaman project: https://github.com/Artikash/Textractor
|
||||
Video tutorial : https://tinyurl.com/textractor-tutorial
|
||||
Video tutorial : https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
Tolong hubungi saya jika kamu memiliki masalah terkait masalah, permintaan fitur, atau pertanyaan terkait Textractor
|
||||
Kamu dapat melakukannya lewat halaman utama project (bagian issues) atau lewat email
|
||||
Source code tersedia dibawah lisensi GPLv3 di halaman utama project
|
||||
@ -568,6 +788,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";
|
||||
CONFIG = u8"Configura gioco";
|
||||
DETACH = u8"Scollega dal gioco";
|
||||
FORGET = u8"Dimentica gioco";
|
||||
ADD_HOOK = u8"Aggiungi gancio";
|
||||
@ -582,6 +803,25 @@ Puoi anche digitare l'ID del processo)";
|
||||
SELECT_PROCESS_INFO = u8"Se digiti manualmente il nome file del processo, si prega di utilizzare il percorso assoluto";
|
||||
FROM_COMPUTER = u8"Seleziona dal computer";
|
||||
PROCESSES = u8"Processi (*.exe)";
|
||||
CODE_INFODUMP = u8R"(Digita il codice di lettura
|
||||
R{S|Q|V|M}[null_length<][codepage#]@addr
|
||||
OR
|
||||
Digita il codice gancio
|
||||
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]]
|
||||
Tutti i numeri eccetto codepage/null_lengt nell'esadecimale
|
||||
Codepage di base è 932 (Shift-JIS) ma può essere cambiato nelle impostazioni
|
||||
A/B: carattere del codepage little/big endian
|
||||
W: carattere UTF-16
|
||||
H: Due byte esadecimali
|
||||
S/Q/V/M: stringa codepage/UTF-16/UTF-8/hex
|
||||
F: tratta le stringhe come linee di testo complete
|
||||
N: non usa il contesto
|
||||
null_lengt: lunghezza del terminatore null utilizzato per la stringa
|
||||
padding: lunghezza del data padding prima della stringa (C struct { int64_t size; char string[500]; } needs padding = 8)
|
||||
Negativi per data_offset/split_offset consulta i registri
|
||||
-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
|
||||
* significa puntatore di deferenziazione+deref_offset)";
|
||||
SAVE_SETTINGS = u8"Salva impostazioni";
|
||||
EXTEN_WINDOW_INSTRUCTIONS = u8R"(Per aggiugnere un estenzione, clicca con il tasto destro la lista estenzioni
|
||||
Alternativamente, trascina e rilascia il file estenzione dal tuo computer
|
||||
@ -593,6 +833,7 @@ Per rimuovere un estenzione, selezionala e premi rimuovi)";
|
||||
CONFIRM_EXTENSION_OVERWRITE = u8"Un'altra versione di questa estenzione esiste già, desidera cancellarla e sovvrascriverla?";
|
||||
EXTENSION_WRITE_ERROR = u8"Impossibile salvare l'estenzione";
|
||||
USE_JP_LOCALE = u8"Emulare l'impostazione locale giapponese?";
|
||||
FAILED_TO_CREATE_CONFIG_FILE = u8"Impossibile creare il file di configurazione \"%1\"";
|
||||
HOOK_SEARCH_UNSTABLE_WARNING = u8"Cercare i ganci è instabile! Preparati a un crash del tuo gioco!";
|
||||
SEARCH_CJK = u8"Cerca per Cinese/Giapponese/Coreano";
|
||||
SEARCH_PATTERN = u8"Cerca schema (matrice byte in esa)";
|
||||
@ -619,17 +860,18 @@ Per rimuovere un estenzione, selezionala e premi rimuovi)";
|
||||
FLUSH_DELAY = u8"Ritardo flush";
|
||||
MAX_BUFFER_SIZE = u8"Massima dimensione buffer";
|
||||
MAX_HISTORY_SIZE = u8"Massima dimensione cronologia";
|
||||
CONFIG_JP_LOCALE = u8"Avvia con il JP locale";
|
||||
CONSOLE = L"Console";
|
||||
CLIPBOARD = L"Appunti";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( creato da me: Artikash (email: akashmozumdar@gmail.com)
|
||||
Pagina principale del progetto: https://github.com/Artikash/Textractor
|
||||
Video tutorial: https://tinyurl.com/textractor-tutorial
|
||||
Video tutorial: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
Contattatemi per ogni problema, richiesta futura, o domande legate a Textractor
|
||||
Puoi farlo attraverso la pagina principale del progetto (sezione issues) o via email
|
||||
Il codice sorgente è disponibile sotto il GPLv3 nella pagina principale
|
||||
Al momento sono in cerca di un nuovo lavoro: contattatemi per email se conoscete qualcuno che ingaggia periti informatici statunitensi
|
||||
Se ti piace questo progetto, parlane con tutti per favore :))";
|
||||
CL_OPTIONS = LR"(utilizzo: Textractor [-p{process id|"process name"}]...
|
||||
CL_OPTIONS = LR"(utilizzo: Textractor [-p{process ID|"process name"}]...
|
||||
esempio: Textractor -p4466 -p"My Game.exe" sta tentando di inniettare i processi con l'ID 4466 o con il nome My Game.exe)";
|
||||
UPDATE_AVAILABLE = L"Aggiornamento disponibile: scaricala da https://github.com/Artikash/Textractor/releases";
|
||||
ALREADY_INJECTED = L"Textractor: già inniettato";
|
||||
@ -644,9 +886,10 @@ esempio: Textractor -p4466 -p"My Game.exe" sta tentando di inniettare i processi
|
||||
REMOVING_HOOK = u8"Textractor: rimuovi gancio: %s";
|
||||
HOOK_FAILED = u8"Textractor: inserimento gancio non riuscito";
|
||||
TOO_MANY_HOOKS = u8"Textractor: troppi ganci: impossibile inserirli";
|
||||
STARTING_SEARCH = u8"Textractor: avvia la ricerca";
|
||||
HOOK_SEARCH_STARTING = u8"Textractor: avvia la ricerca";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: testo insufficente per la ricerca accurata";
|
||||
HOOK_SEARCH_INITIALIZED = u8"Textractor: ricerca inizializzata con %zd ganci";
|
||||
MAKE_GAME_PROCESS_TEXT = u8"Textractor: clicca intorno al gioco per forzarlo nel testo del processo durante i prossimi %d secondi";
|
||||
HOOK_SEARCH_FINISHED = u8"ricerca ganci conclusa, %d risultati trovati";
|
||||
OUT_OF_RECORDS_RETRY = u8"registri di ricerca esauriti, riprova se i risultati sono scarsi (conto registri di base aumentato)";
|
||||
FUNC_MISSING = u8"Textractor: funzione non presente";
|
||||
@ -654,18 +897,51 @@ esempio: Textractor -p4466 -p"My Game.exe" sta tentando di inniettare i processi
|
||||
GARBAGE_MEMORY = u8"Textractor: memoria è in costante cambiamento, inutila la lettura";
|
||||
SEND_ERROR = u8"Textractor: Send ERROR (probabilmente un H-code incorretto)";
|
||||
READ_ERROR = u8"Textractor: Reader ERROR (probabilmente un R-code incorretto)";
|
||||
HIJACK_ERROR = u8"Textractor: Hijack ERROR";
|
||||
HIJACK_ERROR = u8"Textractor: ERRORE di Hijack";
|
||||
COULD_NOT_FIND = u8"Textractor: impossibile trovare il testo";
|
||||
TOO_MANY_TRANS_REQUESTS = L"Troppe richieste di traduzione: rifiuta per farne altre";
|
||||
TRANSLATE_TO = u8"Traduci a";
|
||||
TRANSLATE_SELECTED_THREAD_ONLY = u8"Traduci solo il thread del testo selezionato";
|
||||
RATE_LIMIT_ALL_THREADS = u8"Rate limit tutti i thread del testo";
|
||||
RATE_LIMIT_SELECTED_THREAD = u8"Rate limit thread del testo selezionato";
|
||||
USE_TRANS_CACHE = u8"Utilizza la cache di traduzione";
|
||||
MAX_TRANSLATIONS_IN_TIMESPAN = u8"Numero di token del Rate Limit";
|
||||
TIMESPAN = u8"Token del rate limit ripristina il ritardo (ms)";
|
||||
TOO_MANY_TRANS_REQUESTS = L"Rate limit superato: rifiuta per fare altre richieste di traduzione";
|
||||
TRANSLATION_ERROR = L"Errore durante la traduzione";
|
||||
USE_PREV_SENTENCE_CONTEXT = u8"Utilizza la precedente sentenza come contesto";
|
||||
API_KEY = u8"Chiave API";
|
||||
EXTRA_WINDOW_INFO = u8R"(Tasto destro per cambiare le impostazioni
|
||||
Clicca e trascina i bordi della finestra per muoverla, oppure nell'angolo in basso a destra per ridimensionare)";
|
||||
MAX_SENTENCE_SIZE = u8"Dimensione massima sentenza";
|
||||
TOPMOST = u8"Sempre in primo piano";
|
||||
DICTIONARY = u8"Dizionario";
|
||||
DICTIONARY_INSTRUCTIONS = u8R"(Questo file è utilizzato solo per la funzione "Dizionario" dell'estenzione Extra Window.
|
||||
Utilizza un formato personalizzato spedifico per Textractor e non è pensato per essere critto manualmente.
|
||||
Dovresti cercare per un dizionario in questo formato online (https://github.com/Artikash/Textractor-Dictionaries/releases è un buon posto per cominciare).
|
||||
In alternativa, se sei un progammatore, puoi scrivere uno script per convertire un dizionario da un'altro formato con le informazioni sottostanti.
|
||||
Una volta che hai il dizionario, cerca qualche testo in Extra Window, posizionaci sopra. Puoi scorrere fra tutte le definizioni corrispondenti.
|
||||
Le definizioni sono formattate cosi: |TERM|Hola<<ignored|TERM|hola|TERM|Bonjour|TERM|bonjour|DEFINITION|hello|END|
|
||||
|
||||
Il termine e la definizione può includere rich text (https://doc.qt.io/qt-5/richtext-html-subset.html) che sarà formattato a dovere.
|
||||
Le inflessioni sono formattate cosi: |ROOT|1<<noun|INFLECTS TO|(\w*)s|NAME| plural|END|
|
||||
Textractor controllerà se un termine corrisponde il regex dell'inflessione e se cosi fosse, cercherà ricorsivamente per la radice.
|
||||
La radice è generata rimpiazziando ogni numero con il regex del gruppo di cattura corrispondente (con 0 sostituito dall'intera corrispondenza).
|
||||
Questo processo può risultare facilmente in cicli infiniti e/o in stack overflow. È il tuo compito impedirlo.
|
||||
Il regex dell'inflessione utilizza la sintassi unicode QRegularExpression (https://doc.qt.io/qt-5/qregularexpression.html).
|
||||
Textractor visualizzerà la radice finale cosi come tutte le inflessioni utilizzate per ottenere quella radice.
|
||||
Tuttavia, il testo in un termine dopo << è ignorato quando è visualizzato. Questo è pensato per conservare l'informazione sulle parti del discorso.
|
||||
Questo file deve essere codificato in UTF-8.)";
|
||||
SHOW_ORIGINAL = u8"Testo originale";
|
||||
SHOW_ORIGINAL_INFO = u8R"(Testo originale non sarà mostrato
|
||||
Funziona solo se questa estenzione è usata direttamente dopo un'estensione di traduzione)";
|
||||
SIZE_LOCK = u8"Size lock";
|
||||
ORIGINAL_AFTER_TRANSLATION = u8"Mostra testo originale dopo traduzione";
|
||||
SIZE_LOCK = u8"Lock delle dimensione";
|
||||
POSITION_LOCK = u8"Lock delle posizione";
|
||||
CENTERED_TEXT = u8"Testo centrato";
|
||||
AUTO_RESIZE_WINDOW_HEIGHT = u8"Auto resize altezza finestra";
|
||||
CLICK_THROUGH = u8"Clicca attraverso\tAlt+X";
|
||||
HIDE_MOUSEOVER = u8"Nascondi testo mouseover";
|
||||
OPACITY = u8"Opacità";
|
||||
BG_COLOR = u8"Colore dello sfondo";
|
||||
TEXT_COLOR = u8"Colore del testo";
|
||||
TEXT_OUTLINE = u8"Contorno del testo";
|
||||
@ -687,13 +963,13 @@ Modifiche alle variabili globali da ProcessSentence non sono garantite di persis
|
||||
|
||||
Proprietà in sentenceInfo:
|
||||
"current select": 0 a meno che la sentenza è nel thread di testo attualmente scelto dall'utente.
|
||||
"process id": id del processo che da cui proviene la sentenza. 0 per console e per appunti.
|
||||
"process id": ID del processo che da cui proviene la sentenza. 0 per console e per appunti.
|
||||
"text number": numero dell'attuale thread di testo. Conta uno ad uno quando i thread di testo sono creati. 0 per console, 1 per appunti.
|
||||
--]]
|
||||
function ProcessSentence(sentence, sentenceInfo)
|
||||
--Tuo codice qui...
|
||||
end)";
|
||||
LOAD_LUA_SCRIPT = u8"Carica script";
|
||||
LOAD_SCRIPT = u8"Carica script";
|
||||
LUA_ERROR = L"Errore Lua: %s";
|
||||
REGEX_FILTER = u8"Filtro regex";
|
||||
INVALID_REGEX = u8"Regex non valido";
|
||||
@ -704,7 +980,18 @@ I comandi di rimpiazzo devono essere formattati cosi:
|
||||
Tutto il testo in questo file all'infuori di un comando di rimpiazzo è ignorato.
|
||||
La spaziatura nel testo_originale è ignorato, ma testo_sostituito può contenere spaziature, ritorni a capo, ecc.
|
||||
Questo file deve essere codificato in Unicode (UTF-16 Little Endian).)";
|
||||
THREAD_LINKER = u8"Thread Linker";
|
||||
REGEX_REPLACER_INSTRUCTIONS = LR"(Questo file fa qualcosa solo quando l'estenzione "Regex Replacer" è utilizzata.
|
||||
I comandi di sostituzione devono essere formattati cosi:
|
||||
|REGEX|espressione_regolare|BECOMES|testo_sostituito|MODIFIER|modificatori|END|
|
||||
Il parametro "MODIFIER" può contenere i seguenti modificatori:
|
||||
"g" la sostituzione è globale.
|
||||
"i" la sostituzione ignora maiuscole/minuscole.
|
||||
Se il modificatore è vuoto, la sostituzione viene applicata alla sola prima corrispondenza
|
||||
e fa distinzione tra maiuscole e minuscole.
|
||||
Tutto il testo in questo file all'infuori di un comando di sostituzione è ignorato.
|
||||
Questo file deve essere codificato in Unicode (UTF-16 Little Endian).
|
||||
Apprendere, creare e testare Espressioni Regolari: https://regexr.com/)";
|
||||
THREAD_LINKER = u8"Collegatore di thread";
|
||||
LINK = u8"Collegamento";
|
||||
THREAD_LINK_FROM = u8"Numero di thread da cui collegarsi";
|
||||
THREAD_LINK_TO = u8"Numero di thread a cui collegarsi";
|
||||
@ -743,7 +1030,7 @@ Pressione delete com uma extensão selecionada para removê-la.)";
|
||||
CLIPBOARD = L"Área de Transferência";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( Feito por mim: Artikash (e-mail: akashmozumdar@gmail.com)
|
||||
Homepage do Projeto: https://github.com/Artikash/Textractor
|
||||
Vídeo Tutorial: https://tinyurl.com/textractor-tutorial
|
||||
Vídeo Tutorial: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
Por favor, em caso de problemas, requisição de recurso e/ou funções e de dúvidas, entrar em contato comigo. Use o Inglês para se comunicar.
|
||||
Você pode fazê-lo por meio da Homepage do Projeto (na aba "Issues") ou via E-mail.
|
||||
O código-fonte se encontra disponível na Homepage do projeto sob a licença GPLv3.
|
||||
@ -761,7 +1048,7 @@ Se você gostou desse projeto, divulgue a todos :))";
|
||||
REMOVING_HOOK = u8"Textractor: removendo hook: %s";
|
||||
HOOK_FAILED = u8"Textractor: falha na inserção do hook";
|
||||
TOO_MANY_HOOKS = u8"Textractor: há hooks de mais: não é possível inserir mais";
|
||||
STARTING_SEARCH = u8"Textractor: iniciando busca ";
|
||||
HOOK_SEARCH_STARTING = u8"Textractor: iniciando busca ";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: não há texto suficiente para uma buscar precisa";
|
||||
HOOK_SEARCH_INITIALIZED = u8"Textractor: busca inicializada com %zd hooks";
|
||||
HOOK_SEARCH_FINISHED = u8"Textractor: busca por hooks finalizada, %d resultados encontrados";
|
||||
@ -820,7 +1107,7 @@ Esse arquívo deve ser codifícado em (UTF-16 little endian).)";
|
||||
CLIPBOARD = L"ข้อมูลชั่วคราว";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( ได้ถูกพัฒนาโดย: Artikash (email: akashmozumdar@gmail.com)
|
||||
หน้าเว็บไซต์หลัก: https://github.com/Artikash/Textractor
|
||||
วีดีโอสอนวิถีใช้: https://tinyurl.com/textractor-tutorial
|
||||
วีดีโอสอนวิถีใช้: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
ถ้าหากพบเจอปัญหาสามารถติดต่อมาได้ รวมไปถึงการแนะนำฟังก์ชั้นที่อยากให้มี หรือ คำถามเกี่ยวกับโปรแกรม Textractor สามารถติดต่อ
|
||||
ผ่านหน้าเว็บไซต์หลักผ่านทางหน้า Issue หรือทางอีเมลล์
|
||||
Source code สามารถหาได้จากส่วนของ GPLv3 ที่หน้าหลักของเว็บไซต์)";
|
||||
@ -870,7 +1157,7 @@ Source code สามารถหาได้จากส่วนของ GPLv
|
||||
SETTINGS = u8"설정";
|
||||
EXTENSIONS = u8"확장기능";
|
||||
SELECT_PROCESS = u8"프로세스 선택";
|
||||
ATTACH_INFO = u8R"(부착하려는 게임이 보이지 않는다면, 관리자 권한으로 실행해보세요. 프로세스 id를 입력 할 수도 있습니다.)";
|
||||
ATTACH_INFO = u8R"(부착하려는 게임이 보이지 않는다면, 관리자 권한으로 실행해보세요. 프로세스 ID를 입력 할 수도 있습니다.)";
|
||||
SELECT_PROCESS_INFO = u8"직접 프로세스파일 이름을 타이핑한다면, 정확한 경로를 입력하세요";
|
||||
FROM_COMPUTER = u8"컴퓨터로부터 선택";
|
||||
PROCESSES = u8"프로세스 (*.exe)";
|
||||
@ -933,5 +1220,198 @@ original_text의 빈공간은 무시되지만, replacement_text는 공백과 엔
|
||||
이 파일은 유니코드로 저장되어야만 함 (UTF-16 little endian).)";
|
||||
#endif // KOREAN
|
||||
|
||||
return 0;
|
||||
}();
|
||||
#ifdef FRENCH
|
||||
NATIVE_LANGUAGE = "French";
|
||||
ATTACH = u8"Attacher le jeu";
|
||||
LAUNCH = u8"Lancer le jeu";
|
||||
CONFIG = u8"Configure le jeu";
|
||||
DETACH = u8"Detacher du jeu";
|
||||
FORGET = u8"Oublier le jeu";
|
||||
ADD_HOOK = u8"Ajouter un hook";
|
||||
REMOVE_HOOKS = u8"Enlever un hook(s)";
|
||||
SAVE_HOOKS = u8"Sauvegarder un hook(s)";
|
||||
SEARCH_FOR_HOOKS = u8"Rechercher des hooks";
|
||||
SETTINGS = u8"Paramètres";
|
||||
EXTENSIONS = u8"Extensions";
|
||||
SELECT_PROCESS = u8"Selectionner le processus";
|
||||
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"Sélectionner depuis l'ordinateur";
|
||||
PROCESSES = u8"Processus (*.exe)";
|
||||
CODE_INFODUMP = u8R"(Entrez le read code
|
||||
R{S|Q|V|M}[null_length<][codepage#]@addr
|
||||
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]]
|
||||
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: 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)
|
||||
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
|
||||
Vous pouvez également faire glisser et déposer le fichier d'extension depuis votre ordinateur
|
||||
Pour réorganiser les extensions, faites-les glisser et déposez-les dans la liste
|
||||
(Les extensions sont utilisées de haut en bas: l'ordre est IMPORTANT)
|
||||
Pour supprimer une extension, sélectionnez-la et appuyez sur supprimer)";
|
||||
ADD_EXTENSION = u8"Ajouter une extension";
|
||||
INVALID_EXTENSION = u8"%1 C'est une extension invalide";
|
||||
CONFIRM_EXTENSION_OVERWRITE = u8"Une autre version de cette extension existe déjà. Voulez-vous la supprimer et la remplacer?";
|
||||
EXTENSION_WRITE_ERROR = u8"Impossible d'enregistrer l'extension";
|
||||
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"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_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"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 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)";
|
||||
SHOW_SYSTEM_PROCESSES = u8"Montrer les processus système";
|
||||
DEFAULT_CODEPAGE = u8"Page de code de base";
|
||||
FLUSH_DELAY = u8"Retard de vidage";
|
||||
MAX_BUFFER_SIZE = u8"Taille maximale du tampon";
|
||||
MAX_HISTORY_SIZE = u8"Taille d'historique maximale";
|
||||
CONFIG_JP_LOCALE = u8"Lancement avec les paramètres régionaux JP";
|
||||
CONSOLE = L"Console";
|
||||
CLIPBOARD = L"Presse-papier";
|
||||
ABOUT = L"Textractor " ARCH L" v" VERSION LR"( Fait par moi: Artikash (email: akashmozumdar@gmail.com)
|
||||
Page d'acceuil du projet: https://github.com/Artikash/Textractor
|
||||
Vidéo tuto: https://github.com/Artikash/Textractor/blob/master/docs/TUTORIAL.md
|
||||
FAQ: https://github.com/Artikash/Textractor/wiki/FAQ
|
||||
Veuillez me contacter pour tout problème, demande de fonctionnalité ou question concernant Textractor
|
||||
Vous pouvez le faire via la page d'accueil du projet (section problèmes) ou par e-mail
|
||||
Code source disponible sous GPLv3 sur la page d'accueil du projet
|
||||
Si vous aimez ce projet, parlez-en à tout le monde :))";
|
||||
CL_OPTIONS = LR"(usage: Textractor [-p{process ID|"process name"}]...
|
||||
example: Textractor -p4466 -p"My Game.exe" tries to inject processes with ID 4466 or with name My Game.exe)";
|
||||
UPDATE_AVAILABLE = L"Mise à jour disponible: téléchargez-la depuis https://github.com/Artikash/Textractor/releases";
|
||||
ALREADY_INJECTED = L"Textractor: déjà injecté";
|
||||
NEED_32_BIT = L"Textractor: incompatibilité d'architecture: seul Textractor x86 peut injecter ce processus";
|
||||
NEED_64_BIT = L"Textractor: incompatibilité d'architecture: seul Textractor x64 peut injecter ce processus";
|
||||
INJECT_FAILED = L"Textractor: ne peut pas injecter";
|
||||
LAUNCH_FAILED = L"Textractor: ne peut pas lancer";
|
||||
INVALID_CODE = L"Textractor: code invalide";
|
||||
INVALID_CODEPAGE = L"Textractor: impossible de convertir le texte (page de code non valide?)";
|
||||
PIPE_CONNECTED = u8"Textractor: tuyau connecté";
|
||||
INSERTING_HOOK = u8"Textractor: insertion du hook: %s";
|
||||
REMOVING_HOOK = u8"Textractor: enlève le hook: %s";
|
||||
HOOK_FAILED = u8"Textractor: n'a pas réussi à insérer un hook";
|
||||
TOO_MANY_HOOKS = u8"Textractor: trop de hooks: impossible d'insérer";
|
||||
HOOK_SEARCH_STARTING = u8"Textractor: démarrage de la recherche";
|
||||
NOT_ENOUGH_TEXT = u8"Textractor: pas assez de texte pour effectuer une recherche précise";
|
||||
HOOK_SEARCH_INITIALIZED = u8"Textractor: la recherche a été initialisé avec %zd hooks";
|
||||
MAKE_GAME_PROCESS_TEXT = u8"Textractor: veuillez cliquer dans le jeu pour le forcer à traiter le texte lors de la prochaine %d seconds";
|
||||
HOOK_SEARCH_FINISHED = u8"Textractor: la recherche du hook est finie, %d results found";
|
||||
OUT_OF_RECORDS_RETRY = u8"Textractor: hors des enregistrements de recherche, veuillez réessayer si les résultats sont médiocres (le nombre d'enregistrements par défaut a augmenté)";
|
||||
FUNC_MISSING = u8"Textractor: function non présente";
|
||||
MODULE_MISSING = u8"Textractor: module non présente";
|
||||
GARBAGE_MEMORY = u8"Textractor: mémoire en constante évolution, inutile à lire";
|
||||
SEND_ERROR = u8"Textractor: envoyer ERREUR (comme un H-code instable/incorrect)";
|
||||
READ_ERROR = u8"Textractor: Lire ERREUR (comme un R-code incorrect)";
|
||||
HIJACK_ERROR = u8"Textractor: ERREUR Hijack";
|
||||
COULD_NOT_FIND = u8"Textractor: ne peut pas trouver le texte";
|
||||
TRANSLATE_TO = u8"Traduire à";
|
||||
TRANSLATE_SELECTED_THREAD_ONLY = u8"Traduire uniquement le thread sélectionné";
|
||||
RATE_LIMIT_ALL_THREADS = u8"Taux limite tout les threads de texte";
|
||||
RATE_LIMIT_SELECTED_THREAD = u8"Limite de débit du thread de texte sélectionné";
|
||||
USE_TRANS_CACHE = u8"Utiliser le cache de traduction";
|
||||
MAX_TRANSLATIONS_IN_TIMESPAN = u8"Nombre de tokens du limiteur de débit";
|
||||
TIMESPAN = u8"Délai de restauration du token du limiteur de débit (ms)";
|
||||
TOO_MANY_TRANS_REQUESTS = L"Limite de taux dépassée: refus de faire plus de demande de traduction";
|
||||
TRANSLATION_ERROR = L"Une erreur est survenue pendant la traduction";
|
||||
USE_PREV_SENTENCE_CONTEXT = u8"Utiliser la phrase précédente comme contexte";
|
||||
API_KEY = u8"API key";
|
||||
EXTRA_WINDOW_INFO = u8R"(Clic droit pour modifier les paramètres
|
||||
Cliquez et faites glisser sur les bords de la fenêtre pour vous déplacer ou dans le coin inférieur droit pour redimensionner)";
|
||||
MAX_SENTENCE_SIZE = u8"Taille maximale de la phrase";
|
||||
TOPMOST = u8"Toujours au dessus";
|
||||
DICTIONARY = u8"Dictionnaire";
|
||||
DICTIONARY_INSTRUCTIONS = u8R"(Ce fichier est utilisé uniquement pour la fonction "Dictionnaire" de l'extension Extra Window.
|
||||
Il utilise un format personnalisé spécifique à Textractor et n'est pas destiné à être écrit manuellement.
|
||||
Vous devriez rechercher un dictionnaire dans ce format en ligne (https://github.com/Artikash/Textractor-Dictionaries/releases est un bon point de dépar).
|
||||
Alternativement, si vous êtes programmeur, vous pouvez écrire un script pour convertir un dictionnaire d'un autre format avec les informations ci-dessous.
|
||||
Une fois que vous avez un dictionnaire, pour rechercher du texte dans Extra Window, survolez-le. Vous pouvez faire défiler toutes les définitions correspondantes.
|
||||
Les definitions sont formattés comme ceci:|TERM|Hola<<ignored|TERM|hola|TERM|Bonjour|TERM|bonjour|DEFINITION|hello|END|
|
||||
Le terme et la définition peuvent inclure du texte enrichi (https://doc.qt.io/qt-5/richtext-html-subset.html) qui sera correctement formaté.
|
||||
Les inflexions sont formatées comme ceci:|ROOT|1<<noun|INFLECTS TO|(\w*)s|NAME| plural|END|
|
||||
Textractor vérifiera si un terme correspond à l'expression rationnelle d'inflexion et si c'est le cas, recherchera récursivement le terme racine.
|
||||
Le terme racine est généré en remplaçant chaque nombre par le groupe de capture regex correspondant (0 étant remplacé par la correspondance entière).
|
||||
Ce processus peut facilement entraîner des boucles infinies et / ou des débordements de pile. C'est votre travail d'éviter cela.
|
||||
L'expression regex d'inflexion utilise la syntaxe unicode QRegularExpression (https://doc.qt.io/qt-5/qregularexpression.html).
|
||||
Textractor affichera le terme racine final ainsi que toutes les inflexions utilisées pour arriver à ce terme racine.
|
||||
Cependant, le texte d'un terme après << est ignoré lors de l'affichage. Ceci est destiné à stocker des informations sur une partie de la parole.
|
||||
Ce fichier doit être encodé en UTF-8.)";
|
||||
SHOW_ORIGINAL = u8"Texte Original";
|
||||
SHOW_ORIGINAL_INFO = u8R"(Le texte d'origine ne sera pas affiché
|
||||
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";
|
||||
TEXT_OUTLINE = u8"Contour du texte";
|
||||
OUTLINE_COLOR = u8"Couleur du contour";
|
||||
OUTLINE_SIZE = u8"Taille du contour";
|
||||
OUTLINE_SIZE_INFO = u8"Taille en pixels (recommandé de rester en dessous de 20% de la taille de la police)";
|
||||
FONT = u8"Police";
|
||||
LUA_INTRO = u8R"(--[[
|
||||
ProcessSentence est appelée chaque fois que Textractor reçoit une phrase de texte.
|
||||
Phrase param: phrase reçue par Textractor (UTF-8).
|
||||
Param sentenceInfo: tableau d'informations diverses sur la phrase.
|
||||
Si vous retournez une chaîne, la phrase sera transformée en cette chaîne.
|
||||
Si vous renvoyez zéro, la phrase ne sera pas modifiée.
|
||||
Cette extension utilise plusieurs copies de l'interpréteur Lua pour la sécurité des threads.
|
||||
Les modifications apportées aux variables globales à partir de ProcessSentence ne sont pas garanties de persister.
|
||||
Properties in sentenceInfo:
|
||||
"current select": 0 unless sentence is in the text thread currently selected by the user.
|
||||
"process id": process ID that the sentence is coming from. 0 for console and clipboard.
|
||||
"text number": number of the current text thread. Counts up one by one as text threads are created. 0 for console, 1 for clipboard.
|
||||
--]]
|
||||
function ProcessSentence(sentence, sentenceInfo)
|
||||
--Your code here...
|
||||
end)";
|
||||
LOAD_SCRIPT = u8"Charger le script";
|
||||
LUA_ERROR = L"Erreur Lua: %s";
|
||||
REGEX_FILTER = u8"Filtre regex";
|
||||
INVALID_REGEX = u8"Regex invalide";
|
||||
CURRENT_FILTER = u8"En train de filtrer: %1";
|
||||
REPLACER_INSTRUCTIONS = LR"(Ce fichier ne fait rien lorsque l'extension "Replacer" est utilisée.
|
||||
Les commandes de remplacement doivent être formatées comme:
|
||||
|ORIG|original_text|BECOMES|replacement_text|END|
|
||||
Tout le texte de ce fichier en dehors d'une commande de remplacement est ignoré.
|
||||
Un caret (^) agit comme un caractère générique qui correspond à tout autre caractère unique.
|
||||
Les espaces dans original_text sont ignorés, mais remplacement_text peut contenir des espaces, des nouvelles lignes, etc.
|
||||
Ce fichier doit être encodé en Unicode (UTF-16 Little Endian).)";
|
||||
THREAD_LINKER = u8"Lien du thread";
|
||||
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"Hexadécimal";
|
||||
#endif // FRENCH
|
||||
};
|
||||
|
||||
static auto _ = (Localize(), 0);
|
||||
|
@ -1,32 +1,51 @@
|
||||
include_directories(. util)
|
||||
include_directories(. util minhook/include)
|
||||
|
||||
if(${CMAKE_SIZEOF_VOID_P} EQUAL 8)
|
||||
set(texthook_src
|
||||
main.cc
|
||||
texthook.cc
|
||||
hookfinder.cc
|
||||
engine/match.cc
|
||||
engine/match64.cc
|
||||
engine/native/pchooks.cc
|
||||
util/ithsys/ithsys.cc
|
||||
util/util.cc
|
||||
)
|
||||
set(minhook_src
|
||||
minhook/src/buffer.c
|
||||
minhook/src/hook.c
|
||||
minhook/src/trampoline.c
|
||||
minhook/src/hde/hde64.c
|
||||
)
|
||||
set(texthook_src
|
||||
main.cc
|
||||
texthook.cc
|
||||
hookfinder.cc
|
||||
engine/match.cc
|
||||
engine/match64.cc
|
||||
engine/native/pchooks.cc
|
||||
util/ithsys/ithsys.cc
|
||||
util/util.cc
|
||||
)
|
||||
else()
|
||||
set(texthook_src
|
||||
main.cc
|
||||
texthook.cc
|
||||
hookfinder.cc
|
||||
engine/engine.cc
|
||||
engine/match.cc
|
||||
engine/match32.cc
|
||||
engine/native/pchooks.cc
|
||||
util/util.cc
|
||||
util/ithsys/ithsys.cc
|
||||
util/disasm/disasm.cc
|
||||
util/memdbg/memsearch.cc
|
||||
)
|
||||
set(minhook_src
|
||||
minhook/src/buffer.c
|
||||
minhook/src/hook.c
|
||||
minhook/src/trampoline.c
|
||||
minhook/src/hde/hde32.c
|
||||
)
|
||||
set(texthook_src
|
||||
main.cc
|
||||
texthook.cc
|
||||
hookfinder.cc
|
||||
engine/engine.cc
|
||||
engine/match.cc
|
||||
engine/match32.cc
|
||||
engine/native/pchooks.cc
|
||||
util/util.cc
|
||||
util/ithsys/ithsys.cc
|
||||
util/disasm/disasm.cc
|
||||
util/memdbg/memsearch.cc
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(minhook ${minhook_src})
|
||||
add_library(texthook MODULE ${texthook_src})
|
||||
|
||||
# isn't there a better way to do this?
|
||||
target_precompile_headers(texthook PRIVATE ../include/common.h)
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
target_compile_options(minhook PRIVATE /MT)
|
||||
target_compile_options(texthook PRIVATE /MT)
|
||||
target_link_options(texthook PRIVATE /NODEFAULTLIB:MSVCRT)
|
||||
endif()
|
||||
target_link_libraries(texthook minhook)
|
||||
|
@ -4494,7 +4494,8 @@ bool InsertRUGP1Hook()
|
||||
*/
|
||||
bool InsertRUGP2Hook()
|
||||
{
|
||||
if (!Util::CheckFile(L"vm60.dll") /*|| !SafeFillRange(L"vm60.dll", &low, &high)*/) {
|
||||
auto module = GetModuleHandleW(L"vm60.dll");
|
||||
if (!module /*|| !SafeFillRange(L"vm60.dll", &low, &high)*/) {
|
||||
ConsoleOutput("vnreng:rUGP2: vm60.dll does not exist");
|
||||
return false;
|
||||
}
|
||||
@ -4508,7 +4509,7 @@ bool InsertRUGP2Hook()
|
||||
0x89,0x75, 0x0c // 1001e527 8975 0c mov dword ptr ss:[ebp+0xc],esi
|
||||
};
|
||||
enum { addr_offset = 0x1001e51d - 0x1001e515 };
|
||||
ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStopAddress);
|
||||
ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), (DWORD)module, Util::QueryModuleLimits(module).second);
|
||||
//GROWL_DWORD(addr);
|
||||
if (!addr) {
|
||||
ConsoleOutput("vnreng:rUGP2: pattern not found");
|
||||
@ -4631,20 +4632,15 @@ static void InsertAliceHook2(DWORD addr)
|
||||
// jichi 5/13/2015: Looking for function entries in StoatSpriteEngine.dll
|
||||
bool InsertAliceHook()
|
||||
{
|
||||
DWORD addr;
|
||||
if (addr = (DWORD)GetProcAddress(GetModuleHandleW(L"SACT2.dll"), "SP_TextDraw")) {
|
||||
if (auto addr = Util::FindFunction("SP_TextDraw")) {
|
||||
InsertAliceHook1(addr);
|
||||
return true;
|
||||
}
|
||||
if (addr = (DWORD)GetProcAddress(GetModuleHandleW(L"SACTDX.dll"), "SP_TextDraw")) {
|
||||
InsertAliceHook1(addr);
|
||||
return true;
|
||||
}
|
||||
//if (GetFunctionAddr("SP_SetTextSprite", &addr, &low, &high, 0) && addr) {
|
||||
// InsertAliceHook2(addr);
|
||||
// return true;
|
||||
//}
|
||||
if (addr = (DWORD)GetProcAddress(GetModuleHandleW(L"StoatSpriteEngine.dll"), "SP_SetTextSprite")) { // Artikash 6/27/2018 not sure if this works
|
||||
if (auto addr = Util::FindFunction("SP_SetTextSprite")) { // Artikash 6/27/2018 not sure if this works
|
||||
InsertAliceHook2(addr);
|
||||
return true;
|
||||
}
|
||||
@ -5734,21 +5730,21 @@ void SpecialHookShina2(DWORD esp_base, HookParam *, BYTE, DWORD *data, DWORD *sp
|
||||
// Used to merge correct text thread.
|
||||
// 1. Only keep threads with 0 and -1 split
|
||||
// 2. Skip the thread withb 0 split and with minimum return address
|
||||
void SpecialHookShina1(DWORD esp_base, HookParam *hp, BYTE, DWORD *data, DWORD *split, DWORD *len)
|
||||
{
|
||||
static DWORD min_retaddr = -1;
|
||||
DWORD s = *(DWORD *)(esp_base + hp->split);
|
||||
if (s == 0 || (s & 0xffff) == 0xffff) { // only keep threads with 0 and -1 split
|
||||
if (s == 0 && retof(esp_base) <= min_retaddr) {
|
||||
min_retaddr = retof(esp_base);
|
||||
return;
|
||||
}
|
||||
*split = FIXED_SPLIT_VALUE;
|
||||
// Follow the same logic as the hook.
|
||||
*data = *(DWORD *)*data; // DATA_INDIRECT
|
||||
*len = LeadByteTable[*data & 0xff];
|
||||
}
|
||||
}
|
||||
//void SpecialHookShina1(DWORD esp_base, HookParam *hp, BYTE, DWORD *data, DWORD *split, DWORD *len)
|
||||
//{
|
||||
// static DWORD min_retaddr = -1;
|
||||
// DWORD s = *(DWORD *)(esp_base + hp->split);
|
||||
// if (s == 0 || (s & 0xffff) == 0xffff) { // only keep threads with 0 and -1 split
|
||||
// if (s == 0 && retof(esp_base) <= min_retaddr) {
|
||||
// min_retaddr = retof(esp_base);
|
||||
// return;
|
||||
// }
|
||||
// *split = FIXED_SPLIT_VALUE;
|
||||
// // Follow the same logic as the hook.
|
||||
// *data = *(DWORD *)*data; // DATA_INDIRECT
|
||||
// *len = LeadByteTable[*data & 0xff];
|
||||
// }
|
||||
//}
|
||||
|
||||
// jichi 8/27/2013
|
||||
// Return ShinaRio version number
|
||||
@ -5807,7 +5803,7 @@ bool InsertShinaHook()
|
||||
trigger_fun = [](LPVOID funcAddr, DWORD, DWORD stack)
|
||||
{
|
||||
bool ret = false;
|
||||
if (funcAddr != GetGlyphOutlineA) return false;
|
||||
if (funcAddr != GetGlyphOutlineA && funcAddr != GetTextExtentPoint32A) return false;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
// Address of text is somewhere on stack in call to func. Search for it.
|
||||
@ -5821,7 +5817,7 @@ bool InsertShinaHook()
|
||||
hp.type = DIRECT_READ;
|
||||
hp.address = addr;
|
||||
ConsoleOutput("Textractor: triggered: adding dynamic reader");
|
||||
NewHook(hp, "READ");
|
||||
NewHook(hp, "ShinaRio READ");
|
||||
ret = true;
|
||||
}
|
||||
};
|
||||
@ -5829,9 +5825,8 @@ bool InsertShinaHook()
|
||||
return ret;
|
||||
};
|
||||
ConsoleOutput("Textractor: ShinaRio 2.50+: adding trigger");
|
||||
return true;
|
||||
}
|
||||
else if (ver >= 48) { // v2.48, v2.49
|
||||
if (ver >= 48) { // v2.48, v2.49
|
||||
HookParam hp = {};
|
||||
hp.address = (DWORD)::GetTextExtentPoint32A;
|
||||
hp.text_fun = SpecialHookShina2;
|
||||
@ -5984,10 +5979,11 @@ bool InsertWaffleDynamicHook(LPVOID addr, DWORD frame, DWORD stack)
|
||||
* Sample game: 完全時間停止 体験版
|
||||
* GDI text: TextOutA and GetTextExtentPoint32A
|
||||
*/
|
||||
void InsertWaffleHook()
|
||||
bool InsertWaffleHook()
|
||||
{
|
||||
bool found = false;
|
||||
for (DWORD i = processStartAddress + 0x1000; i < processStopAddress - 4; i++)
|
||||
if (*(DWORD *)i == 0xac68) {
|
||||
if (*(DWORD *)i == 0xac68 && *(BYTE*)(i + 4) == 0) {
|
||||
HookParam hp = {};
|
||||
hp.address = i;
|
||||
hp.length_offset = 1;
|
||||
@ -5997,10 +5993,38 @@ void InsertWaffleHook()
|
||||
hp.type = DATA_INDIRECT|USING_SPLIT;
|
||||
ConsoleOutput("vnreng: INSERT WAFFLE");
|
||||
NewHook(hp, "WAFFLE");
|
||||
return;
|
||||
found = true;
|
||||
}
|
||||
|
||||
/** new waffle?
|
||||
* test on 母三人とアナあそび https://vndb.org/v24214
|
||||
* and 変態エルフ姉妹と真面目オーク https://vndb.org/v24215
|
||||
* and いかにして俺の妻は孕んだか…… https://vndb.org/v26205
|
||||
* and 俺の知らぬ間に彼女が… https://vndb.org/v27781
|
||||
*/
|
||||
const BYTE bytes[] = {
|
||||
0x50, //50 push eax
|
||||
0x8b, 0xce, //8BCE mov ecx,esi
|
||||
0xc6, 0x45, 0xfc, XX, //C645 FC 01 move byte ptr ss:[ebp-4],?
|
||||
0x89, 0x75, 0xd4, //8975 D4 move dword ptr ss:[ebp-0x2c],esi
|
||||
0xe8, XX4, //E8 ?? call ??
|
||||
0x8d, 0x45, 0xdc //8D45 DC lea eax,dword ptr ss:[ebp-0x24]
|
||||
};
|
||||
if (DWORD addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStopAddress))
|
||||
{
|
||||
HookParam hp = {};
|
||||
hp.address = addr;
|
||||
hp.offset = pusha_eax_off - 4;
|
||||
hp.index = 0x00;
|
||||
hp.length_offset = 1;
|
||||
hp.type = DATA_INDIRECT;
|
||||
ConsoleOutput("Textractor: INSERT WAFFLE2");
|
||||
NewHook(hp, "WAFFLE2");
|
||||
found = true;
|
||||
}
|
||||
//ConsoleOutput("Probably Waffle. Wait for text.");
|
||||
trigger_fun = InsertWaffleDynamicHook;
|
||||
if (!found) trigger_fun = InsertWaffleDynamicHook;
|
||||
return found;
|
||||
//ConsoleOutput("vnreng:WAFFLE: failed");
|
||||
}
|
||||
|
||||
@ -6585,6 +6609,173 @@ bool InsertNitroplusHook()
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jazzinghen 23/05/2020: Add TokyoNecro hook
|
||||
*
|
||||
* [Nitroplus] 東京Necro 1.01 - Text boxes hook
|
||||
*
|
||||
* Hook code: HS-14*8@B5420:TokyoNecro.exe
|
||||
*
|
||||
* Debug method:
|
||||
* Found memory location where the text was written, then used hardware break on write.
|
||||
* After that found the function that writes the text in, found that the memory pointed
|
||||
* contains more than just the text. Followed the call stack "upwards" until a function
|
||||
* that handles only the text copy is found.
|
||||
*
|
||||
* Disassembled code:
|
||||
* TokyoNecro.exe+B5420 - 55 - push ebp ; place to hook
|
||||
* TokyoNecro.exe+B5421 - 8B EC - mov ebp,esp
|
||||
* TokyoNecro.exe+B5423 - 6A FF - push -01
|
||||
* TokyoNecro.exe+B5425 - 68 E8613000 - push TokyoNecro.exe+1961E8
|
||||
* TokyoNecro.exe+B542A - 64 A1 00000000 - mov eax,fs:[00000000]
|
||||
* TokyoNecro.exe+B5430 - 50 - push eax
|
||||
* TokyoNecro.exe+B5431 - 64 89 25 00000000 - mov fs:[00000000],esp
|
||||
* TokyoNecro.exe+B5438 - 83 EC 1C - sub esp,1C
|
||||
* TokyoNecro.exe+B543B - 8B 55 08 - mov edx,[ebp+08]
|
||||
* TokyoNecro.exe+B543E - 53 - push ebx
|
||||
* TokyoNecro.exe+B543F - 56 - push esi
|
||||
* TokyoNecro.exe+B5440 - 8B C2 - mov eax,edx
|
||||
* TokyoNecro.exe+B5442 - 57 - push edi
|
||||
* TokyoNecro.exe+B5443 - 8B D9 - mov ebx,ecx
|
||||
* TokyoNecro.exe+B5445 - C7 45 EC 0F000000 - mov [ebp-14],0000000F
|
||||
* TokyoNecro.exe+B544C - C7 45 E8 00000000 - mov [ebp-18],00000000
|
||||
* TokyoNecro.exe+B5453 - C6 45 D8 00 - mov byte ptr [ebp-28],00
|
||||
* TokyoNecro.exe+B5457 - 8D 70 01 - lea esi,[eax+01]
|
||||
* TokyoNecro.exe+B545A - 8D 9B 00000000 - lea ebx,[ebx+00000000]
|
||||
* TokyoNecro.exe+B5460 - 8A 08 - mov cl,[eax]
|
||||
* TokyoNecro.exe+B5462 - 40 - inc eax
|
||||
* TokyoNecro.exe+B5463 - 84 C9 - test cl,cl
|
||||
* TokyoNecro.exe+B5465 - 75 F9 - jne TokyoNecro.exe+B5460
|
||||
* TokyoNecro.exe+B5467 - 2B C6 - sub eax,esi
|
||||
* TokyoNecro.exe+B5469 - 52 - push edx
|
||||
* TokyoNecro.exe+B546A - 8B F8 - mov edi,eax ▷ Search
|
||||
* TokyoNecro.exe+B546C - 8D 75 D8 - lea esi,[ebp-28] |
|
||||
* TokyoNecro.exe+B546F - E8 6CE1F4FF - call TokyoNecro.exe+35E0 ▷
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
* There's more data above due to the fact that the start of the function is very
|
||||
* common and it was hooking a wrong function.
|
||||
*
|
||||
* The text is contained into the memory location at [esp+04] when hooking the
|
||||
* code at TokyoNecro.exe+B5420
|
||||
*
|
||||
* If the game is hooked right at the main menu it will also catch the real time clock
|
||||
* rendered there.
|
||||
*/
|
||||
|
||||
namespace TokyoNecro {
|
||||
|
||||
const BYTE funcSig[] = { 0x55, 0x8b, 0xec };
|
||||
|
||||
bool TextHook() {
|
||||
|
||||
const BYTE bytecodes[] = {
|
||||
0x8B, 0xF8, // 8B F8 - mov edi,eax
|
||||
0x8D, 0x75, 0xD8, // 8D 75 D8 - lea esi,[ebp-28]
|
||||
0xE8, 0x6C, 0xE1, 0xF4, 0xFF, // E8 6CE1F4FF - call TokyoNecro.exe+35E0
|
||||
};
|
||||
ULONG addr = MemDbg::findBytes(bytecodes, sizeof(bytecodes), processStartAddress, processStopAddress);
|
||||
if (addr == 0) {
|
||||
ConsoleOutput("Textractor:TokyoNecro: pattern not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for the start of the function
|
||||
const ULONG function_start = MemDbg::findEnclosingAlignedFunction(addr);
|
||||
if (memcmp((void*)function_start, funcSig, sizeof(funcSig)) != 0) {
|
||||
ConsoleOutput("Textractor: TokyoNecro: function start not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
HookParam hp = {};
|
||||
hp.address = function_start;
|
||||
// The memory address is held at [ebp+08] at TokyoNecro.exe+B543B, meaning that at
|
||||
// the start of the function it's right above the stack pointer. Since there's no
|
||||
// way to do an operation on the value of a register BEFORE dereferencing (e.g.
|
||||
// (void*)(esp+4) instead of ((void*)esp)+4) we have to go up the stack instead of
|
||||
// using the data in the registers
|
||||
hp.offset = 0x4;
|
||||
hp.type = USING_STRING;
|
||||
ConsoleOutput("Textractor: INSERT TokyoNecroText");
|
||||
NewHook(hp, "TokyoNecroText");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Nitroplus] 東京Necro 1.01 - Database/Encyclopedia hook
|
||||
*
|
||||
* Hook code: HS4*@B5380:tokyonecro.exe
|
||||
*
|
||||
* TokyoNecro.exe+B5380 - 55 - push ebp ; Location to hook
|
||||
* TokyoNecro.exe+B5381 - 8B EC - mov ebp,esp
|
||||
* TokyoNecro.exe+B5383 - 6A FF - push -01
|
||||
* TokyoNecro.exe+B5385 - 68 E8618E00 - push TokyoNecro.exe+1961E8
|
||||
* TokyoNecro.exe+B538A - 64 A1 00000000 - mov eax,fs:[00000000]
|
||||
* TokyoNecro.exe+B5390 - 50 - push eax
|
||||
* TokyoNecro.exe+B5391 - 64 89 25 00000000 - mov fs:[00000000],esp
|
||||
* TokyoNecro.exe+B5398 - 83 EC 1C - sub esp,1C
|
||||
* TokyoNecro.exe+B539B - 8B 55 08 - mov edx,[ebp+08]
|
||||
* TokyoNecro.exe+B539E - 53 - push ebx
|
||||
* TokyoNecro.exe+B539F - 56 - push esi
|
||||
* TokyoNecro.exe+B53A0 - 8B C2 - mov eax,edx
|
||||
* TokyoNecro.exe+B53A2 - 57 - push edi
|
||||
* TokyoNecro.exe+B53A3 - 8B D9 - mov ebx,ecx
|
||||
* TokyoNecro.exe+B53A5 - C7 45 EC 0F000000 - mov [ebp-14],0000000F
|
||||
* TokyoNecro.exe+B53AC - C7 45 E8 00000000 - mov [ebp-18],00000000
|
||||
* TokyoNecro.exe+B53B3 - C6 45 D8 00 - mov byte ptr [ebp-28],00
|
||||
* TokyoNecro.exe+B53B7 - 8D 70 01 - lea esi,[eax+01]
|
||||
* TokyoNecro.exe+B53BA - 8D 9B 00000000 - lea ebx,[ebx+00000000]
|
||||
* TokyoNecro.exe+B53C0 - 8A 08 - mov cl,[eax]
|
||||
* TokyoNecro.exe+B53C2 - 40 - inc eax
|
||||
* TokyoNecro.exe+B53C3 - 84 C9 - test cl,cl
|
||||
* TokyoNecro.exe+B53C5 - 75 F9 - jne TokyoNecro.exe+B53C0
|
||||
* TokyoNecro.exe+B53C7 - 2B C6 - sub eax,esi
|
||||
* TokyoNecro.exe+B53C9 - 52 - push edx
|
||||
* TokyoNecro.exe+B53CA - 8B F8 - mov edi,eax ▷ Search
|
||||
* TokyoNecro.exe+B53CC - 8D 75 D8 - lea esi,[ebp-28] |
|
||||
* TokyoNecro.exe+B53CF - E8 0CE2F4FF - call TokyoNecro.exe+35E0 ▷
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
bool DatabaseHook()
|
||||
{
|
||||
const BYTE bytecodes[] = {
|
||||
0x8B, 0xF8, // 8B F8 - mov edi,eax
|
||||
0x8D, 0x75, 0xD8, // 8D 75 D8 - lea esi,[ebp-28]
|
||||
0xE8, 0x0C, 0xE2, 0xF4, 0xFF, // E8 6CE1F4FF - call TokyoNecro.exe+35E0
|
||||
};
|
||||
ULONG addr = MemDbg::findBytes(bytecodes, sizeof(bytecodes), processStartAddress, processStopAddress);
|
||||
if (addr == 0) {
|
||||
ConsoleOutput("vnreng:TokyoNecro: pattern not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for the start of the function
|
||||
const ULONG function_start = MemDbg::findEnclosingAlignedFunction(addr);
|
||||
if (memcmp((void*)function_start, funcSig, sizeof(funcSig)) != 0) {
|
||||
ConsoleOutput("Textractor: TokyoNecro: function start not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
HookParam hp = {};
|
||||
hp.address = function_start;
|
||||
hp.offset = 0x4;
|
||||
hp.type = USING_STRING;
|
||||
NewHook(hp, "TokyoNecroDatabase");
|
||||
ConsoleOutput("vnreng: INSERT TokyoNecroDatabase");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace TokyoNecro
|
||||
|
||||
bool InsertTokyoNecroHook()
|
||||
{
|
||||
TokyoNecro::DatabaseHook();
|
||||
return TokyoNecro::TextHook();
|
||||
}
|
||||
|
||||
// jichi 6/21/2015
|
||||
namespace { // unnamed
|
||||
|
||||
@ -9409,7 +9600,31 @@ static bool InsertNewWillPlusHook()
|
||||
NewHook(hp, "WillPlus2");
|
||||
found = true;
|
||||
}
|
||||
if (!found) ConsoleOutput("Textractor: WillPlus2: failed to find instructions");
|
||||
/*
|
||||
hook cmp esi,0x3000
|
||||
Sample games:
|
||||
https://vndb.org/r54549
|
||||
https://vndb.org/v22705
|
||||
https://vndb.org/v24852
|
||||
https://vndb.org/v25719
|
||||
https://vndb.org/v27227
|
||||
https://vndb.org/v27385
|
||||
*/
|
||||
const BYTE pattern[] =
|
||||
{
|
||||
0x81,0xfe,0x00,0x30,0x00,0x00 //81FE 00300000 cmp esi,0x3000
|
||||
};
|
||||
for (auto addr : Util::SearchMemory(pattern, sizeof(pattern), PAGE_EXECUTE, processStartAddress, processStopAddress))
|
||||
{
|
||||
HookParam hp = {};
|
||||
hp.address = addr;
|
||||
hp.type = USING_UNICODE;
|
||||
hp.offset = pusha_esi_off - 4;
|
||||
hp.length_offset = 1;
|
||||
NewHook(hp, "WillPlus3");
|
||||
found = true;
|
||||
}
|
||||
if (!found) ConsoleOutput("Textractor: WillPlus: failed to find instructions");
|
||||
return found;
|
||||
}
|
||||
|
||||
@ -9873,7 +10088,7 @@ BYTE JIS_tableL[0x80] = {
|
||||
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x00,
|
||||
};
|
||||
|
||||
void SpecialHookAnex86(DWORD esp_base, HookParam *hp, BYTE, DWORD *data, DWORD *split, DWORD *len)
|
||||
void SpecialHookAnex86(DWORD esp_base, HookParam*, BYTE, DWORD *data, DWORD *split, DWORD *len)
|
||||
{
|
||||
__asm
|
||||
{
|
||||
@ -9912,19 +10127,28 @@ _fin:
|
||||
} // unnamed namespace
|
||||
bool InsertAnex86Hook()
|
||||
{
|
||||
const DWORD dwords[] = {0x618ac033,0x0d418a0c}; // jichi 12/25/2013: Remove static keyword
|
||||
for (DWORD i = processStartAddress + 0x1000; i < processStopAddress - 8; i++)
|
||||
if (*(DWORD *)i == dwords[0])
|
||||
if (*(DWORD *)(i + 4) == dwords[1]) {
|
||||
const BYTE bytes[] = {
|
||||
0x8a, XX, 0x0c, // mov ??,[ecx+0C]
|
||||
0x8a, XX, 0x0d // mov ??,[ecx+0D]
|
||||
};
|
||||
bool found = false;
|
||||
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStopAddress)) {
|
||||
//const DWORD dwords[] = {0x618ac033,0x0d418a0c}; // jichi 12/25/2013: Remove static keyword
|
||||
//for (DWORD i = processStartAddress + 0x1000; i < processStopAddress - 8; i++)
|
||||
//if (*(DWORD *)i == dwords[0])
|
||||
//if (*(DWORD *)(i + 4) == dwords[1]) {
|
||||
HookParam hp = {};
|
||||
hp.address = i;
|
||||
if (*(BYTE*)(addr - 2) == 0x33 || *(BYTE*)(addr - 2) == 0x31) addr = addr - 2;
|
||||
hp.address = addr;
|
||||
hp.offset = pusha_ecx_off - 4;
|
||||
hp.text_fun = SpecialHookAnex86;
|
||||
//hp.type = EXTERN_HOOK;
|
||||
hp.length_offset = 1;
|
||||
ConsoleOutput("vnreng: INSERT Anex86");
|
||||
NewHook(hp, "Anex86");
|
||||
return true;
|
||||
found = true;
|
||||
}
|
||||
if (found) return true;
|
||||
ConsoleOutput("vnreng:Anex86: failed");
|
||||
return false;
|
||||
}
|
||||
@ -10550,8 +10774,59 @@ bool InsertArtemis2Hook()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InsertArtemis3Hook()
|
||||
{
|
||||
const BYTE bytes[] = {
|
||||
0x55, // 005FD780 | 55 | push ebp |
|
||||
0x8B, 0xEC, // 005FD781 | 8BEC | mov ebp,esp |
|
||||
0x83, 0xE4, 0xF8, // 005FD783 | 83E4 F8 | and esp,FFFFFFF8 |
|
||||
0x83, 0xEC, 0x3C, // 005FD786 | 83EC 3C | sub esp,3C |
|
||||
0xA1, XX4, // 005FD789 | A1 6C908600 | mov eax,dword ptr ds:[86906C] |
|
||||
0x33, 0xC4, // 005FD78E | 33C4 | xor eax,esp |
|
||||
0x89, 0x44, 0x24, 0x38, // 005FD790 | 894424 38 | mov dword ptr ss:[esp+38],eax |
|
||||
0x53, // 005FD794 | 53 | push ebx |
|
||||
0x56, // 005FD795 | 56 | push esi |
|
||||
0x8B, 0xC1, // 005FD796 | 8BC1 | mov eax,ecx |
|
||||
0xC7, 0x44, 0x24, 0x14, 0x00, 0x00, 0x00, 0x00, // 005FD798 | C74424 14 00000000 | mov dword ptr ss:[esp+14],0 |
|
||||
0x8B, 0x4D, 0x0C, // 005FD7A0 | 8B4D 0C | mov ecx,dword ptr ss:[ebp+C] |
|
||||
0x33, 0xF6, // 005FD7A3 | 33F6 | xor esi,esi |
|
||||
0x57, // 005FD7A5 | 57 | push edi |
|
||||
0x8B, 0x7D, 0x08, // 005FD7A6 | 8B7D 08 | mov edi,dword ptr ss:[ebp+8] |
|
||||
0x89, 0x44, 0x24, 0x14, // 005FD7A9 | 894424 14 | mov dword ptr ss:[esp+14],eax |
|
||||
0x89, 0x4C, 0x24, 0x28, // 005FD7AD | 894C24 28 | mov dword ptr ss:[esp+28],ecx |
|
||||
0x80, 0x3F, 0x00, // 005FD7B1 | 803F 00 | cmp byte ptr ds:[edi],0 |
|
||||
0x0F, 0x84, XX4, // 005FD7B4 | 0F84 88040000 | je ヘンタイ・プリズンsplit 1.5FDC42 |
|
||||
0x83, 0xB8, XX4, 0x00, // 005FD7BA | 83B8 74030000 00 | cmp dword ptr ds:[eax+374],0 |
|
||||
0x8B, 0xDF, // 005FD7C1 | 8BDF | mov ebx,edi |
|
||||
};
|
||||
|
||||
enum { addr_offset = 0 }; // distance to the beginning of the function, which is 0x55 (push ebp)
|
||||
ULONG range = min(processStopAddress - processStartAddress, MAX_REL_ADDR);
|
||||
ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStartAddress + range);
|
||||
if (!addr) {
|
||||
ConsoleOutput("Textractor:Artemis3: pattern not found");
|
||||
return false;
|
||||
}
|
||||
addr += addr_offset;
|
||||
enum { push_ebp = 0x55 }; // beginning of the function
|
||||
if (*(BYTE *)addr != push_ebp) {
|
||||
ConsoleOutput("Textractor:Artemis3: beginning of the function not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
HookParam hp = {};
|
||||
hp.address = addr;
|
||||
hp.offset = pusha_ebx_off - 4;
|
||||
hp.type = USING_UTF8;
|
||||
|
||||
ConsoleOutput("Textractor: INSERT Artemis3");
|
||||
NewHook(hp, "Artemis3");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InsertArtemisHook()
|
||||
{ return InsertArtemis1Hook() || InsertArtemis2Hook(); }
|
||||
{ return InsertArtemis1Hook() || InsertArtemis2Hook() || InsertArtemis3Hook(); }
|
||||
|
||||
/**
|
||||
* jichi 1/2/2014: Taskforce2 Engine
|
||||
@ -13786,6 +14061,9 @@ bool InsertHorkEyeHook()
|
||||
return true;
|
||||
}
|
||||
|
||||
memcpy(spDefault.pattern, Array<BYTE>{ 0xcc, 0xcc, 0xcc, XX, 0xec }, spDefault.length = 5);
|
||||
spDefault.offset = 3;
|
||||
|
||||
const BYTE bytes2[] =
|
||||
{
|
||||
0x83, 0xec, XX, // sub esp,??
|
||||
@ -16165,19 +16443,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
|
||||
@ -16667,30 +16945,40 @@ bool InsertAdobeFlash10Hook()
|
||||
*/
|
||||
bool InsertRenpyHook()
|
||||
{
|
||||
for (int pythonMinorVersion = 0; pythonMinorVersion <= 8; ++pythonMinorVersion)
|
||||
{
|
||||
wchar_t python[] = L"python2X.dll";
|
||||
python[7] = L'0' + pythonMinorVersion;
|
||||
if (HMODULE module = GetModuleHandleW(python))
|
||||
{
|
||||
wcscpy_s(spDefault.exportModule, python);
|
||||
HookParam hp = {};
|
||||
hp.address = (DWORD)GetProcAddress(module, "PyUnicodeUCS2_Format");
|
||||
if (!hp.address)
|
||||
{
|
||||
ConsoleOutput("Textractor: Ren'py failed: failed to find PyUnicodeUCS2_Format");
|
||||
return false;
|
||||
}
|
||||
hp.offset = 4;
|
||||
hp.index = 0xc;
|
||||
hp.length_offset = 0;
|
||||
hp.split = pusha_ebx_off - 4;
|
||||
hp.type = USING_STRING | USING_UNICODE | NO_CONTEXT | DATA_INDIRECT | USING_SPLIT;
|
||||
//hp.filter_fun = [](void* str, auto, auto, auto) { return *(wchar_t*)str != L'%'; };
|
||||
NewHook(hp, "Ren'py");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
wchar_t python[] = L"python2X.dll", libpython[] = L"libpython2.X.dll";
|
||||
for (wchar_t* name : { python, libpython })
|
||||
{
|
||||
wchar_t* pos = wcschr(name, L'X');
|
||||
for (int pythonMinorVersion = 0; pythonMinorVersion <= 8; ++pythonMinorVersion)
|
||||
{
|
||||
*pos = L'0' + pythonMinorVersion;
|
||||
if (HMODULE module = GetModuleHandleW(name))
|
||||
{
|
||||
wcscpy_s(spDefault.exportModule, name);
|
||||
HookParam hp = {};
|
||||
hp.address = (DWORD)GetProcAddress(module, "PyUnicodeUCS2_Format");
|
||||
if (!hp.address)
|
||||
{
|
||||
ConsoleOutput("Textractor: Ren'py failed: failed to find PyUnicodeUCS2_Format");
|
||||
return false;
|
||||
}
|
||||
hp.offset = 4;
|
||||
hp.index = 0xc;
|
||||
hp.length_offset = 0;
|
||||
//hp.split = pusha_ebx_off - 4;
|
||||
hp.text_fun = [](auto, auto, auto, DWORD* data, DWORD* split, DWORD* count)
|
||||
{
|
||||
*data = *(DWORD*)(*data + 0xc);
|
||||
*count = wcslen((wchar_t*)*data) * sizeof(wchar_t);
|
||||
*split = wcschr((wchar_t*)*data, L'%') == nullptr;
|
||||
};
|
||||
hp.type = USING_STRING | USING_UNICODE | NO_CONTEXT | DATA_INDIRECT/* | USING_SPLIT*/;
|
||||
//hp.filter_fun = [](void* str, auto, auto, auto) { return *(wchar_t*)str != L'%'; };
|
||||
NewHook(hp, "Ren'py");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ConsoleOutput("Textractor: Ren'py failed: failed to find python2X.dll");
|
||||
return false;
|
||||
}
|
||||
@ -16713,7 +17001,7 @@ void InsertMonoHook(HMODULE h)
|
||||
if (!getDomain || !getName || !getJitInfo) goto failed;
|
||||
static auto domain = getDomain();
|
||||
if (!domain) goto failed;
|
||||
ConsoleOutput("Textractor: Mono Dynamic ENTER (hooks = %s)", loadedConfig ? loadedConfig : "brute force");
|
||||
ConsoleOutput("Textractor: Mono Dynamic ENTER (hooks = %s)", *loadedConfig ? loadedConfig : "brute force");
|
||||
const BYTE prolog[] = { 0x55, 0x8b, 0xec };
|
||||
for (auto addr : Util::SearchMemory(prolog, sizeof(prolog), PAGE_EXECUTE_READWRITE))
|
||||
{
|
||||
@ -16728,7 +17016,7 @@ void InsertMonoHook(HMODULE h)
|
||||
HookParam hp = {};
|
||||
hp.address = addr;
|
||||
hp.type = USING_UNICODE | FULL_STRING;
|
||||
if (!loadedConfig) hp.type |= KNOWN_UNSTABLE;
|
||||
if (!*loadedConfig) hp.type |= KNOWN_UNSTABLE;
|
||||
hp.offset = 4;
|
||||
char nameForUser[HOOK_NAME_SIZE] = {};
|
||||
strncpy_s(nameForUser, name + 1, HOOK_NAME_SIZE - 1);
|
||||
@ -16746,7 +17034,7 @@ void InsertMonoHook(HMODULE h)
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
}(addr);
|
||||
}
|
||||
if (!loadedConfig) ConsoleOutput("Textractor: Mono Dynamic used brute force: if performance issues arise, please specify the correct hook in the game configuration");
|
||||
if (!*loadedConfig) ConsoleOutput("Textractor: Mono Dynamic used brute force: if performance issues arise, please specify the correct hook in the game configuration");
|
||||
return true;
|
||||
failed:
|
||||
ConsoleOutput("Textractor: Mono Dynamic failed");
|
||||
@ -16803,7 +17091,7 @@ bool InsertMonoHooks()
|
||||
if (FARPROC addr = ::GetProcAddress(h, func.functionName)) {
|
||||
hp.address = (DWORD)addr;
|
||||
hp.type = func.hookType;
|
||||
if (loadedConfig) hp.type |= HOOK_EMPTY;
|
||||
if (*loadedConfig) hp.type |= HOOK_EMPTY;
|
||||
hp.filter_fun = NoAsciiFilter;
|
||||
hp.offset = func.textIndex * 4;
|
||||
hp.length_offset = func.lengthIndex * 4;
|
||||
|
@ -14,7 +14,7 @@ namespace Engine {
|
||||
// Global variables
|
||||
extern wchar_t *processName, // cached
|
||||
processPath[MAX_PATH]; // cached
|
||||
inline const char *requestedEngine = nullptr, *loadedConfig = nullptr;
|
||||
inline const char *requestedEngine = "", * loadedConfig = "";
|
||||
|
||||
bool InsertMonoHooks(); // Mono
|
||||
|
||||
@ -125,6 +125,7 @@ bool InsertNeXASHook(); // NeXAS: Thumbnail.pac
|
||||
bool InsertNextonHook(); // NEXTON: aInfo.db
|
||||
bool InsertNexton1Hook();
|
||||
bool InsertNitroplusHook(); // Nitroplus: *.npa
|
||||
bool InsertTokyoNecroHook(); // Nitroplus TokyoNecro: *.npk, resource string
|
||||
bool InsertPalHook(); // AMUSE CRAFT: *.pac
|
||||
bool InsertPensilHook(); // Pensil: PSetup.exe
|
||||
bool InsertPONScripterHook();
|
||||
@ -163,7 +164,7 @@ void InsertRyokuchaHook(); // Ryokucha: _checksum.exe
|
||||
void InsertRealliveHook(); // RealLive: RealLive*.exe
|
||||
void InsertStuffScriptHook(); // Stuff: *.mpk
|
||||
bool InsertTinkerBellHook(); // TinkerBell: arc00.dat
|
||||
void InsertWaffleHook(); // WAFFLE: cg.pak
|
||||
bool InsertWaffleHook(); // WAFFLE: cg.pak
|
||||
|
||||
// CIRCUS: avdata/
|
||||
bool InsertCircusHook1();
|
||||
|
@ -13,7 +13,7 @@ namespace Engine
|
||||
WCHAR* processName, // cached
|
||||
processPath[MAX_PATH]; // cached
|
||||
|
||||
char configFileData[1000];
|
||||
char configFileData[1000]{};
|
||||
|
||||
bool UnsafeDetermineEngineType();
|
||||
|
||||
@ -35,7 +35,7 @@ namespace Engine
|
||||
|
||||
void Hijack()
|
||||
{
|
||||
static auto _ = []
|
||||
static auto _ = ([]
|
||||
{
|
||||
GetModuleFileNameW(nullptr, processPath, MAX_PATH);
|
||||
processName = wcsrchr(processPath, L'\\') + 1;
|
||||
@ -44,14 +44,15 @@ namespace Engine
|
||||
wcscpy_s(wcsrchr(configFilename, L'\\') + 1, std::size(GAME_CONFIG_FILE), GAME_CONFIG_FILE);
|
||||
if (AutoHandle<> configFile = CreateFileW(configFilename, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL))
|
||||
{
|
||||
if (ReadFile(configFile, configFileData, sizeof(configFileData) - 1, DUMMY, nullptr)) ConsoleOutput("Textractor: game configuration loaded");
|
||||
if (strncmp(configFileData, "Engine:", 7) == 0)
|
||||
ReadFile(configFile, configFileData, sizeof(configFileData) - 1, DUMMY, nullptr);
|
||||
if (strnicmp(configFileData, "engine:", 7) == 0)
|
||||
{
|
||||
if (loadedConfig = strchr(configFileData, '\n')) *(char*)loadedConfig++ = 0;
|
||||
ConsoleOutput("Textractor: Engine = %s", requestedEngine = configFileData + 7);
|
||||
if (const char* config = strchr(configFileData, '\n')) *(char*)(loadedConfig = config)++ = 0;
|
||||
ConsoleOutput("Textractor: Engine = %s", requestedEngine = strlwr(configFileData + 7));
|
||||
}
|
||||
else loadedConfig = configFileData;
|
||||
if (loadedConfig && !*loadedConfig) loadedConfig = nullptr;
|
||||
if (!*loadedConfig || strstr(configFileData, "https://")) loadedConfig = "";
|
||||
else ConsoleOutput("Textractor: game configuration loaded");
|
||||
}
|
||||
|
||||
processStartAddress = processStopAddress = (uintptr_t)GetModuleHandleW(nullptr);
|
||||
@ -66,14 +67,14 @@ namespace Engine
|
||||
spDefault.maxAddress = processStopAddress;
|
||||
ConsoleOutput("Textractor: hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress);
|
||||
|
||||
DetermineEngineType();
|
||||
return NULL;
|
||||
}();
|
||||
if (!strstr(requestedEngine, "none")) DetermineEngineType();
|
||||
if (processStartAddress + 0x40000 > processStopAddress) ConsoleOutput("Textractor: WARNING injected process is very small, possibly a dummy!");
|
||||
}(), 0);
|
||||
}
|
||||
|
||||
bool ShouldMonoHook(const char* name)
|
||||
{
|
||||
if (!loadedConfig) return strstr(name, "string:") && !strstr(name, "string:mem");
|
||||
if (!*loadedConfig) return strstr(name, "string:") && !strstr(name, "string:mem");
|
||||
for (const char* hook = loadedConfig; hook; hook = strchr(hook + 1, '\t'))
|
||||
for (auto start = name; *start; ++start)
|
||||
for (int i = 0; ; ++i)
|
||||
|
@ -435,6 +435,22 @@ bool DetermineEngineByFile4()
|
||||
return true;
|
||||
}
|
||||
|
||||
// jichi 11/22/2015: 凍京NECRO 体験版
|
||||
// Jazzinghen 23/05/2020: Add check for 凍京NECRO
|
||||
// ResEdit shows multiple potential strings:
|
||||
// - TOKYONECRO
|
||||
// - 東京NECRO
|
||||
// - TokyoNecro.exe in "OriginalFilename"
|
||||
if (Util::CheckFile(L"*.npk")) {
|
||||
if (Util::SearchResourceString(L"TOKYONECRO")) {
|
||||
InsertTokyoNecroHook();
|
||||
}
|
||||
else {
|
||||
ConsoleOutput("vnreng: IGNORE new Nitroplus");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -613,10 +629,9 @@ bool DetermineEngineAtLast()
|
||||
}
|
||||
if (Util::CheckFile(L"MovieTexture.dll") && (InsertPensilHook() || Insert2RMHook())) // MovieTexture.dll also exists in 2RM games such as 母子愛2体験版, which is checked first
|
||||
return true;
|
||||
if ((Util::CheckFile(L"system") && Util::CheckFile(L"system.dat")) || Util::CheckFile(L"*01")) { // jichi 7/31/2015 & Artikash 6/15/2018
|
||||
InsertAbelHook();
|
||||
return true;
|
||||
}
|
||||
if ((Util::CheckFile(L"system") && Util::CheckFile(L"system.dat")) || Util::CheckFile(L"*01")) // jichi 7/31/2015 & Artikash 6/15/2018
|
||||
if (InsertAbelHook())
|
||||
return true;
|
||||
if (Util::CheckFile(L"data\\*.cpk")) { // jichi 12/2/2014
|
||||
Insert5pbHook();
|
||||
return true;
|
||||
@ -709,12 +724,6 @@ bool DetermineNoEngine()
|
||||
return true;
|
||||
}
|
||||
|
||||
// jichi 11/22/2015: 凍京NECRO 体験版
|
||||
if (Util::CheckFile(L"*.npk")) {
|
||||
ConsoleOutput("vnreng: IGNORE new Nitroplus");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 8/29/2015 jichi: minori, text in GetGlyphOutlineA
|
||||
if (Util::CheckFile(L"*.paz")) {
|
||||
ConsoleOutput("vnreng: IGNORE minori");
|
||||
|
@ -80,7 +80,7 @@ namespace Engine
|
||||
if (!getDomain || !getName || !getJitInfo) goto failed;
|
||||
static auto domain = getDomain();
|
||||
if (!domain) goto failed;
|
||||
ConsoleOutput("Textractor: Mono Dynamic ENTER (hooks = %s)", loadedConfig ? loadedConfig : "brute force");
|
||||
ConsoleOutput("Textractor: Mono Dynamic ENTER (hooks = %s)", *loadedConfig ? loadedConfig : "brute force");
|
||||
const BYTE prolog1[] = { 0x55, 0x48, 0x8b, 0xec };
|
||||
const BYTE prolog2[] = { 0x48, 0x83, 0xec };
|
||||
for (auto [prolog, size] : Array<const BYTE*, size_t>{ { prolog1, sizeof(prolog1) }, { prolog2, sizeof(prolog2) } })
|
||||
@ -97,7 +97,7 @@ namespace Engine
|
||||
HookParam hp = {};
|
||||
hp.address = addr;
|
||||
hp.type = USING_STRING | USING_UNICODE | FULL_STRING;
|
||||
if (!loadedConfig) hp.type |= KNOWN_UNSTABLE;
|
||||
if (!*loadedConfig) hp.type |= KNOWN_UNSTABLE;
|
||||
hp.offset = -0x20; // rcx
|
||||
hp.padding = 20;
|
||||
char nameForUser[HOOK_NAME_SIZE] = {};
|
||||
@ -119,7 +119,7 @@ namespace Engine
|
||||
}(addr);
|
||||
}
|
||||
|
||||
if (!loadedConfig) ConsoleOutput("Textractor: Mono Dynamic used brute force: if performance issues arise, please specify the correct hook in the game configuration");
|
||||
if (!*loadedConfig) ConsoleOutput("Textractor: Mono Dynamic used brute force: if performance issues arise, please specify the correct hook in the game configuration");
|
||||
return true;
|
||||
failed:
|
||||
ConsoleOutput("Textractor: Mono Dynamic failed");
|
||||
@ -132,25 +132,85 @@ namespace Engine
|
||||
// sample game https://www.freem.ne.jp/dl/win/18963
|
||||
bool InsertV8Hook(HMODULE module)
|
||||
{
|
||||
if (uint64_t addr = (uint64_t)GetProcAddress(module, "?Write@String@v8@@QEBAHPEAGHHH@Z"))
|
||||
auto getV8Length = [](uintptr_t, uintptr_t data)
|
||||
{
|
||||
int len = *(int*)(data - 4);
|
||||
return len > 0 && len < PIPE_BUFFER_SIZE ? len * 2 : 0;
|
||||
};
|
||||
|
||||
uint64_t addr1 = (uint64_t)GetProcAddress(module, "?Write@String@v8@@QEBAHPEAGHHH@Z"),
|
||||
// Artikash 6/7/2021: Add new hook for new version of V8 used by RPG Maker MZ
|
||||
addr2 = (uint64_t)GetProcAddress(module, "??$WriteToFlat@G@String@internal@v8@@SAXV012@PEAGHH@Z");
|
||||
|
||||
if (addr1 || addr2)
|
||||
{
|
||||
std::tie(spDefault.minAddress, spDefault.maxAddress) = Util::QueryModuleLimits(module);
|
||||
spDefault.maxRecords = Util::SearchMemory(spDefault.pattern, spDefault.length, PAGE_EXECUTE, spDefault.minAddress, spDefault.maxAddress).size() * 20;
|
||||
ConsoleOutput("Textractor: JavaScript hook is known to be low quality: try searching for hooks if you don't like it");
|
||||
}
|
||||
if (addr1)
|
||||
{
|
||||
HookParam hp = {};
|
||||
hp.type = USING_STRING | USING_UNICODE | DATA_INDIRECT;
|
||||
hp.address = addr;
|
||||
hp.address = addr1;
|
||||
hp.offset = -0x20; // rcx
|
||||
hp.index = 0;
|
||||
hp.padding = 23;
|
||||
hp.length_fun = [](uintptr_t, uintptr_t data)
|
||||
{
|
||||
int len = *(int*)(data - 4);
|
||||
return len > 0 && len < PIPE_BUFFER_SIZE ? len * 2 : 0;
|
||||
};
|
||||
hp.length_fun = getV8Length;
|
||||
NewHook(hp, "JavaScript");
|
||||
return true;
|
||||
}
|
||||
if (addr2)
|
||||
{
|
||||
HookParam hp = {};
|
||||
hp.type = USING_STRING | USING_UNICODE;
|
||||
hp.address = addr2;
|
||||
hp.offset = -0x20; // rcx
|
||||
hp.padding = 11;
|
||||
hp.length_fun = getV8Length;
|
||||
NewHook(hp, "JavaScript");
|
||||
}
|
||||
return addr1 || addr2;
|
||||
}
|
||||
|
||||
/** Artikash 8/10/2018: Ren'py
|
||||
*
|
||||
* Sample games: https://vndb.org/v19843 https://vndb.org/v12038 and many more OELVNs
|
||||
*
|
||||
* Uses CPython, and links to python27.dll. PyUicodeUCS2_Format is the function used to process text.
|
||||
* first argument. offset 0x18 from that is a wchar_t* to the actual string
|
||||
* ebx seems to work well as the split param, not sure why
|
||||
*/
|
||||
bool InsertRenpyHook()
|
||||
{
|
||||
wchar_t python[] = L"python2X.dll", libpython[] = L"libpython2.X.dll";
|
||||
for (wchar_t* name : { python, libpython })
|
||||
{
|
||||
wchar_t* pos = wcschr(name, L'X');
|
||||
for (int pythonMinorVersion = 0; pythonMinorVersion <= 8; ++pythonMinorVersion)
|
||||
{
|
||||
*pos = L'0' + pythonMinorVersion;
|
||||
if (HMODULE module = GetModuleHandleW(name))
|
||||
{
|
||||
wcscpy_s(spDefault.exportModule, name);
|
||||
HookParam hp = {};
|
||||
hp.address = (DWORD)GetProcAddress(module, "PyUnicodeUCS2_Format");
|
||||
if (!hp.address)
|
||||
{
|
||||
ConsoleOutput("Textractor: Ren'py failed: failed to find PyUnicodeUCS2_Format");
|
||||
return false;
|
||||
}
|
||||
hp.offset = -0x20; // rcx
|
||||
hp.index = 0x18;
|
||||
hp.length_offset = 0;
|
||||
//hp.split = pusha_ebx_off - 4;
|
||||
hp.type = USING_STRING | USING_UNICODE | NO_CONTEXT | DATA_INDIRECT /* | USING_SPLIT*/;
|
||||
//hp.filter_fun = [](void* str, auto, auto, auto) { return *(wchar_t*)str != L'%'; };
|
||||
NewHook(hp, "Ren'py");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ConsoleOutput("Textractor: Ren'py failed: failed to find python2X.dll");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -168,6 +228,8 @@ namespace Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Util::CheckFile(L"*.py") && InsertRenpyHook()) return true;
|
||||
|
||||
for (const wchar_t* monoName : { L"mono.dll", L"mono-2.0-bdwgc.dll" }) if (HMODULE module = GetModuleHandleW(monoName)) if (InsertMonoHooks(module)) return true;
|
||||
|
||||
for (std::wstring DXVersion : { L"d3dx9", L"d3dx10" })
|
||||
|
@ -2,8 +2,10 @@
|
||||
#include "defs.h"
|
||||
#include "main.h"
|
||||
#include "util.h"
|
||||
#include "MinHook.h"
|
||||
|
||||
extern const char* STARTING_SEARCH;
|
||||
extern const char* HOOK_SEARCH_STARTING;
|
||||
extern const char* HOOK_SEARCH_INITIALIZING;
|
||||
extern const char* HOOK_SEARCH_INITIALIZED;
|
||||
extern const char* MAKE_GAME_PROCESS_TEXT;
|
||||
extern const char* HOOK_SEARCH_FINISHED;
|
||||
@ -68,8 +70,8 @@ namespace
|
||||
// https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention
|
||||
// https://stackoverflow.com/questions/43358429/save-value-of-xmm-registers
|
||||
0x48, 0x83, 0xec, 0x20, // sub rsp,0x20
|
||||
0xc5, 0xfa, 0x7f, 0x24, 0x24, // vmovdqu [rsp],xmm4
|
||||
0xc5, 0xfa, 0x7f, 0x6c, 0x24, 0x10, // vmovdqu [rsp+0x10],xmm5
|
||||
0xf3, 0x0f, 0x7f, 0x24, 0x24, // movdqu [rsp],xmm4
|
||||
0xf3, 0x0f, 0x7f, 0x6c, 0x24, 0x10, // movdqu [rsp+0x10],xmm5
|
||||
0x48, 0x8d, 0x8c, 0x24, 0xa8, 0x00, 0x00, 0x00, // lea rcx,[rsp+0xa8]
|
||||
0x48, 0xba, 0,0,0,0,0,0,0,0, // mov rcx,@addr
|
||||
0x48, 0xb8, 0,0,0,0,0,0,0,0, // mov rax,@Send
|
||||
@ -77,8 +79,8 @@ namespace
|
||||
0x48, 0x83, 0xe4, 0xf0, // and rsp,0xfffffffffffffff0 ; align stack
|
||||
0xff, 0xd0, // call rax
|
||||
0x48, 0x89, 0xdc, // mov rsp,rbx
|
||||
0xc5, 0xfa, 0x6f, 0x6c, 0x24, 0x10, // vmovdqu xmm5,XMMWORD PTR[rsp + 0x10]
|
||||
0xc5, 0xfa, 0x6f, 0x24, 0x24, // vmovdqu xmm4,XMMWORD PTR[rsp]
|
||||
0xf3, 0x0f, 0x6f, 0x6c, 0x24, 0x10, // movdqu xmm5,XMMWORD PTR[rsp + 0x10]
|
||||
0xf3, 0x0f, 0x6f, 0x24, 0x24, // movdqu xmm4,XMMWORD PTR[rsp]
|
||||
0x48, 0x83, 0xc4, 0x20, // add rsp,0x20
|
||||
0x41, 0x5f, // pop r15
|
||||
0x41, 0x5e, // pop r14
|
||||
@ -97,7 +99,7 @@ namespace
|
||||
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]
|
||||
0,0,0,0,0,0,0,0 // @original
|
||||
};
|
||||
constexpr int addr_offset = 50, send_offset = 60, original_offset = 126, registers = 16;
|
||||
@ -200,12 +202,12 @@ void SearchForHooks(SearchParam spUser)
|
||||
|
||||
sp = spUser.length == 0 ? spDefault : spUser;
|
||||
|
||||
ConsoleOutput(HOOK_SEARCH_INITIALIZING, 0.);
|
||||
do
|
||||
try { records = std::make_unique<HookRecord[]>(recordsAvailable = sp.maxRecords); }
|
||||
catch (std::bad_alloc) { ConsoleOutput("Textractor: SearchForHooks ERROR: out of memory, retrying to allocate %d", sp.maxRecords /= 2); }
|
||||
while (!records && sp.maxRecords);
|
||||
|
||||
ConsoleOutput(STARTING_SEARCH);
|
||||
std::vector<uint64_t> addresses;
|
||||
if (*sp.boundaryModule) std::tie(sp.minAddress, sp.maxAddress) = Util::QueryModuleLimits(GetModuleHandleW(sp.boundaryModule));
|
||||
if (*sp.exportModule) addresses = GetFunctions((uintptr_t)GetModuleHandleW(sp.exportModule));
|
||||
@ -222,11 +224,13 @@ void SearchForHooks(SearchParam spUser)
|
||||
MH_CreateHook((void*)addresses[i], trampolines[i], &original);
|
||||
MH_QueueEnableHook((void*)addresses[i]);
|
||||
memcpy(trampolines[i], trampoline, sizeof(trampoline));
|
||||
*(uintptr_t*)(trampolines[i] + addr_offset) = addresses[i];
|
||||
*(uintptr_t*)(trampolines[i] + addr_offset) = addresses[i];
|
||||
*(void**)(trampolines[i] + original_offset) = original;
|
||||
if (i % 2500 == 0) ConsoleOutput(HOOK_SEARCH_INITIALIZING, 1 + 98. * i / addresses.size());
|
||||
}
|
||||
ConsoleOutput(HOOK_SEARCH_INITIALIZED, addresses.size());
|
||||
MH_ApplyQueued();
|
||||
ConsoleOutput(HOOK_SEARCH_STARTING);
|
||||
ConsoleOutput(MAKE_GAME_PROCESS_TEXT, sp.searchTime / 1000);
|
||||
Sleep(sp.searchTime);
|
||||
for (auto addr : addresses) MH_QueueDisableHook((void*)addr);
|
||||
@ -261,7 +265,7 @@ void SearchForText(wchar_t* text, UINT codepage)
|
||||
char codepageText[PATTERN_SIZE * 4] = {};
|
||||
WideCharToMultiByte(codepage, 0, text, PATTERN_SIZE, codepageText, PATTERN_SIZE * 4, nullptr, nullptr);
|
||||
if (strlen(utf8Text) < 4 || strlen(codepageText) < 4 || wcslen(text) < 4) return ConsoleOutput(NOT_ENOUGH_TEXT);
|
||||
ConsoleOutput(STARTING_SEARCH);
|
||||
ConsoleOutput(HOOK_SEARCH_STARTING);
|
||||
auto GenerateHooks = [&](std::vector<uint64_t> addresses, HookParamType type)
|
||||
{
|
||||
for (auto addr : addresses)
|
||||
|
@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include "types.h"
|
||||
|
||||
void SearchForText(wchar_t* text, UINT codepage);
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include "texthook.h"
|
||||
#include "hookfinder.h"
|
||||
#include "util.h"
|
||||
#include "MinHook.h"
|
||||
|
||||
extern const char* PIPE_CONNECTED;
|
||||
extern const char* INSERTING_HOOK;
|
||||
|
@ -4,7 +4,6 @@
|
||||
// 8/23/2013 jichi
|
||||
// Branch: ITH/IHF_DLL.h, rev 66
|
||||
|
||||
#include "common.h"
|
||||
#include "types.h"
|
||||
|
||||
void TextOutput(ThreadParam tp, BYTE (*buffer)[PIPE_BUFFER_SIZE], int len);
|
||||
@ -16,49 +15,6 @@ void RemoveHook(uint64_t addr, int maxOffset = 9);
|
||||
|
||||
inline SearchParam spDefault;
|
||||
|
||||
extern "C" // minhook library
|
||||
{
|
||||
enum MH_STATUS
|
||||
{
|
||||
MH_OK,
|
||||
MH_ERROR_ALREADY_INITIALIZED,
|
||||
MH_ERROR_NOT_INITIALIZED,
|
||||
MH_ERROR_ALREADY_CREATED,
|
||||
MH_ERROR_NOT_CREATED,
|
||||
MH_ERROR_ENABLED,
|
||||
MH_ERROR_DISABLED,
|
||||
MH_ERROR_NOT_EXECUTABLE,
|
||||
MH_ERROR_UNSUPPORTED_FUNCTION,
|
||||
MH_ERROR_MEMORY_ALLOC,
|
||||
MH_ERROR_MEMORY_PROTECT,
|
||||
MH_ERROR_MODULE_NOT_FOUND,
|
||||
MH_ERROR_FUNCTION_NOT_FOUND
|
||||
};
|
||||
|
||||
MH_STATUS WINAPI MH_Initialize(VOID);
|
||||
MH_STATUS WINAPI MH_Uninitialize(VOID);
|
||||
|
||||
// Creates a Hook for the specified target function, in disabled state.
|
||||
// Parameters:
|
||||
// pTarget [in] A pointer to the target function, which will be
|
||||
// overridden by the detour function.
|
||||
// pDetour [in] A pointer to the detour function, which will override
|
||||
// the target function.
|
||||
// ppOriginal [out] A pointer to the trampoline function, which will be
|
||||
// used to call the original target function.
|
||||
// This parameter can be NULL.
|
||||
MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal);
|
||||
MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget);
|
||||
MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget);
|
||||
MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget);
|
||||
MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget);
|
||||
MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget);
|
||||
MH_STATUS WINAPI MH_ApplyQueued(VOID);
|
||||
const char* WINAPI MH_StatusToString(MH_STATUS status);
|
||||
}
|
||||
|
||||
#define MH_ALL_HOOKS NULL
|
||||
|
||||
#define ITH_RAISE (*(int*)0 = 0) // raise C000005, for debugging only
|
||||
#define ITH_TRY __try
|
||||
#define ITH_EXCEPT __except(EXCEPTION_EXECUTE_HANDLER)
|
||||
|
1
texthook/minhook
Submodule
1
texthook/minhook
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 423d1e45af2ed2719a5c31e990e935ef301ed9c3
|
@ -6,6 +6,7 @@
|
||||
#include "texthook.h"
|
||||
#include "main.h"
|
||||
#include "ithsys/ithsys.h"
|
||||
#include "MinHook.h"
|
||||
|
||||
extern const char* FUNC_MISSING;
|
||||
extern const char* MODULE_MISSING;
|
||||
@ -30,7 +31,7 @@ namespace { // unnamed
|
||||
0xff, 0xd3, // call ebx
|
||||
0x9d, // popfd
|
||||
0x61, // popad
|
||||
0x9d, // popfd
|
||||
0x9d, // popfd
|
||||
0x68, 0,0,0,0, // push @original
|
||||
0xc3 // ret ; basically absolute jmp to @original
|
||||
};
|
||||
@ -57,8 +58,8 @@ namespace { // unnamed
|
||||
// https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention
|
||||
// https://stackoverflow.com/questions/43358429/save-value-of-xmm-registers
|
||||
0x48, 0x83, 0xec, 0x20, // sub rsp,0x20
|
||||
0xc5, 0xfa, 0x7f, 0x24, 0x24, // vmovdqu [rsp],xmm4
|
||||
0xc5, 0xfa, 0x7f, 0x6c, 0x24, 0x10, // vmovdqu [rsp+0x10],xmm5
|
||||
0xf3, 0x0f, 0x7f, 0x24, 0x24, // movdqu [rsp],xmm4
|
||||
0xf3, 0x0f, 0x7f, 0x6c, 0x24, 0x10, // movdqu [rsp+0x10],xmm5
|
||||
0x48, 0x8d, 0x94, 0x24, 0xa8, 0x00, 0x00, 0x00, // lea rdx,[rsp+0xa8]
|
||||
0x48, 0xb9, 0,0,0,0,0,0,0,0, // mov rcx,@this
|
||||
0x48, 0xb8, 0,0,0,0,0,0,0,0, // mov rax,@TextHook::Send
|
||||
@ -66,8 +67,8 @@ namespace { // unnamed
|
||||
0x48, 0x83, 0xe4, 0xf0, // and rsp,0xfffffffffffffff0 ; align stack
|
||||
0xff, 0xd0, // call rax
|
||||
0x48, 0x89, 0xdc, // mov rsp,rbx
|
||||
0xc5, 0xfa, 0x6f, 0x6c, 0x24, 0x10, // vmovdqu xmm5,XMMWORD PTR[rsp + 0x10]
|
||||
0xc5, 0xfa, 0x6f, 0x24, 0x24, // vmovdqu xmm4,XMMWORD PTR[rsp]
|
||||
0xf3, 0x0f, 0x6f, 0x6c, 0x24, 0x10, // movdqu xmm5,XMMWORD PTR[rsp + 0x10]
|
||||
0xf3, 0x0f, 0x6f, 0x24, 0x24, // movdqu xmm4,XMMWORD PTR[rsp]
|
||||
0x48, 0x83, 0xc4, 0x20, // add rsp,0x20
|
||||
0x41, 0x5f, // pop r15
|
||||
0x41, 0x5e, // pop r14
|
||||
@ -86,7 +87,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]
|
||||
0,0,0,0,0,0,0,0 // @original
|
||||
};
|
||||
int this_offset = 50, send_offset = 60, original_offset = 126;
|
||||
@ -192,6 +193,7 @@ void TextHook::Send(uintptr_t dwDataBase)
|
||||
|
||||
TextOutput(tp, buffer, count);
|
||||
#endif // _WIN64
|
||||
++*pbData;
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
@ -307,7 +309,7 @@ int TextHook::GetLength(uintptr_t base, uintptr_t in)
|
||||
else {
|
||||
if (hp.type & BIG_ENDIAN)
|
||||
in >>= 8;
|
||||
len = LeadByteTable[in & 0xff]; //Slightly faster than IsDBCSLeadByte
|
||||
len = !!IsDBCSLeadByteEx(hp.codepage, in & 0xff) + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
// 8/24/2013 TODO:
|
||||
// - Clean up this file
|
||||
// - Reduce global variables. Use namespaces or singleton classes instead.
|
||||
#include "common.h"
|
||||
#include "types.h"
|
||||
|
||||
// Artikash 6/17/2019 TODO: These have the wrong values on x64
|
||||
@ -52,6 +51,6 @@ private:
|
||||
|
||||
};
|
||||
|
||||
enum { MAX_HOOK = 300, HOOK_BUFFER_SIZE = MAX_HOOK * sizeof(TextHook), HOOK_SECTION_SIZE = HOOK_BUFFER_SIZE * 2 };
|
||||
enum { MAX_HOOK = 2500, HOOK_BUFFER_SIZE = MAX_HOOK * sizeof(TextHook), HOOK_SECTION_SIZE = HOOK_BUFFER_SIZE * 2 };
|
||||
|
||||
// EOF
|
||||
|
@ -9,32 +9,6 @@
|
||||
#include "ithsys/ithsys.h"
|
||||
#include "const.h"
|
||||
|
||||
// - Global variables -
|
||||
|
||||
// jichi 6/12/2015: https://en.wikipedia.org/wiki/Shift_JIS
|
||||
// Leading table for SHIFT-JIS encoding
|
||||
BYTE LeadByteTable[0x100] = {
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1
|
||||
};
|
||||
|
||||
// - API functions -
|
||||
|
||||
extern "C" {
|
||||
/**
|
||||
* Return the address of the first matched pattern.
|
||||
* Artikash 7/14/2018: changed implementation, hopefully it behaves the same
|
||||
@ -70,15 +44,25 @@ DWORD IthGetMemoryRange(LPCVOID mem, DWORD *base, DWORD *size)
|
||||
return info.Protect > PAGE_NOACCESS;
|
||||
}
|
||||
|
||||
inline DWORD GetHash(LPSTR str)
|
||||
{
|
||||
DWORD hash = 0;
|
||||
//for (; *str; str++)
|
||||
while (*str)
|
||||
hash = ((hash >> 7) | (hash << 25)) + *str++;
|
||||
return hash;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
// jichi 6/12/2015: https://en.wikipedia.org/wiki/Shift_JIS
|
||||
// Leading table for SHIFT-JIS encoding
|
||||
BYTE LeadByteTable[0x100] = {
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1
|
||||
};
|
||||
|
||||
// EOF
|
@ -10,14 +10,10 @@
|
||||
//#include "ntdll/ntdll.h"
|
||||
#include <Windows.h>
|
||||
|
||||
// jichi 8/24/2013: Why extern "C"? Any specific reason to use C instead of C++ naming?
|
||||
extern "C" {
|
||||
|
||||
// jichi 10/1/2013: Return 0 if failed. So, it is ambiguous if the search pattern starts at 0
|
||||
DWORD SearchPattern(DWORD base, DWORD base_length, LPCVOID search, DWORD search_length); // KMP
|
||||
|
||||
DWORD IthGetMemoryRange(LPCVOID mem, DWORD *base, DWORD *size);
|
||||
} // extern "C"
|
||||
|
||||
extern BYTE LeadByteTable[];
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
#include "util/util.h"
|
||||
#include "ithsys/ithsys.h"
|
||||
#include "main.h"
|
||||
#include <Psapi.h>
|
||||
|
||||
namespace { // unnamed
|
||||
|
||||
@ -345,6 +346,15 @@ std::vector<uint64_t> SearchMemory(const void* bytes, short length, DWORD protec
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
uintptr_t FindFunction(const char* function)
|
||||
{
|
||||
static HMODULE modules[300] = {};
|
||||
static auto _ = EnumProcessModules(GetCurrentProcess(), modules, sizeof(modules), DUMMY);
|
||||
for (auto module : modules) if (auto addr = GetProcAddress(module, function)) return (uintptr_t)addr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// EOF
|
||||
|
@ -3,8 +3,6 @@
|
||||
// util.h
|
||||
// 8/23/2013 jichi
|
||||
|
||||
#include "common.h"
|
||||
|
||||
namespace Util {
|
||||
|
||||
bool unloadCurrentModule();
|
||||
@ -24,6 +22,7 @@ bool SearchResourceString(LPCWSTR str);
|
||||
|
||||
std::pair<uint64_t, uint64_t> QueryModuleLimits(HMODULE module);
|
||||
std::vector<uint64_t> SearchMemory(const void* bytes, short length, DWORD protect = PAGE_EXECUTE, uintptr_t minAddr = 0, uintptr_t maxAddr = -1ULL);
|
||||
uintptr_t FindFunction(const char* function);
|
||||
|
||||
} // namespace Util
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user