Compare commits

...

262 Commits

Author SHA1 Message Date
Akash Mozumdar
df5b830d17 revert the dumbest change I ever made 2023-08-26 07:36:48 -04:00
Akash Mozumdar
f3fbe04409 standardize file names 2023-04-24 14:48:16 -04:00
Glaringsoul
5adeb1fab7 Update README_GER.md 2023-04-24 14:40:55 -04:00
Glaringsoul
5c7b41dd84 Create README_GER.md
Translated the README.MD by hand into German.

Will probably tackle the text.ccp next, although I haven't looked at it, so I cannot really say how long that can take.

Some parts especially words that are commonly used in German as Anglicisms are left as is, this is not an oversight by me, but intentional.
2023-04-24 14:40:55 -04:00
秋庭里香
92e2e85191 Update Chinese (Simplified) translation
and add a link to text.cpp in the README.md
2023-04-24 14:34:40 -04:00
Blu3train
0bc23ba2c7 update ShinaRio 2.50+ 2023-04-24 14:28:49 -04:00
Akash Mozumdar
fd8a090e28 remove workaround for fixed msvc bug 2023-04-21 19:43:32 -04:00
Akash Mozumdar
aa00d9e789 ignore extra parameters #947 2023-04-21 19:42:54 -04:00
Akash Mozumdar
cfa11c5646 use drive relative path 2023-04-21 19:23:14 -04:00
Akash Mozumdar
446f6d44e6 upgrade build configuration to not rely on qt creator which is no longer required by qt installer 2023-04-21 18:31:17 -04:00
Akash Mozumdar
ff1a7a358c handle toggling of cache properly, error cases consistently, and stop saving cache unnecessarily 2023-04-21 17:23:25 -04:00
Akash Mozumdar
60a5d74eda improve config loading and prevent crashes 2023-04-21 16:49:22 -04:00
Akash Mozumdar
4cebe9462f insane hack to work around false positive in SearchForText 2023-04-21 16:41:00 -04:00
Akash Mozumdar
d97ebd4fbb edge is also usable for devtools 2023-04-21 16:35:19 -04:00
Akash Mozumdar
233e75225c add link to stylesheets 2022-09-10 20:01:48 -04:00
Akash Mozumdar
72ad51f039 tighten abel detection 2022-08-09 00:02:27 -04:00
Akash Mozumdar
39fcfaf644 restore shinario hook 2022-08-08 23:13:51 -04:00
RoARene317
5eb0440f01 Fix some typos 2022-08-08 18:39:16 -06:00
RoARene317
c4ca84d39b Change from text --> Teks in Indonesian 2022-08-08 18:39:16 -06:00
RoARene317
c7b818aa75 Removing some english and fix translations
Fix some Indonesian translations that still english to make it Indonesian but still understandable for young people.
2022-08-08 18:39:16 -06:00
Blu3train
2cb45ab9e3 DeepL Translate: Added Turkish and Indonesian 2022-07-26 23:14:57 -06:00
Blu3train
72132239a5 new browser size setting for DeepL blocking in hidden mode 2022-07-26 22:29:29 -06:00
Blu3train
5eaafbe81c Devtools extensions: chrome user data cache error if there are spaces in path 2022-07-26 22:29:29 -06:00
Akash Mozumdar
97f2bc6304 change qt version to make alpha builds compatible with releases 2022-07-26 23:59:26 -04:00
Akash Mozumdar
f732f488e6
update translation tasks 2022-01-31 05:05:03 -07:00
Blu3train
3e283975c8 Fix auto detect source language selection 2022-01-31 01:18:10 -07:00
Akash Mozumdar
5b35f121d2 reorganize and update docs 2022-01-31 01:16:53 -07:00
Akash Mozumdar
b28aa3d189 clarify text 2022-01-31 00:48:50 -07:00
Akash Mozumdar
faeda02f2b allow users to type capital x 2022-01-30 01:00:00 -07:00
Akash Mozumdar
ef55ef0c61 improve chrome locating and ignore empty translation 2022-01-29 14:20:57 -07:00
Akash Mozumdar
c9240d9e68 improve process selection 2022-01-29 12:49:48 -07:00
Akash Mozumdar
6ba4f9c909 fix incorrect reading 2022-01-23 00:52:15 -07:00
Akash Mozumdar
7c9f861926 most people won't bother with styler 2022-01-23 00:30:57 -07:00
Akash Mozumdar
e158219266 small corrections 2022-01-23 00:29:48 -07:00
Akash Mozumdar
3a9fdfb9ae fix error messaging 2022-01-02 00:50:25 -07:00
Akash Mozumdar
dad036083a destroy garbage sentences 2022-01-01 22:06:10 -07:00
Blu3train
b09783d111 Moved test after filter 2022-01-01 22:06:10 -07:00
Blu3train
306b9f0525 Don't translate text consisting of spaces only 2022-01-01 22:06:10 -07:00
Akash Mozumdar
823987e79f fix crash on windows 11 2022-01-01 22:05:53 -07:00
Akash Mozumdar
fdc8a13b36 clarify instructions 2022-01-01 22:05:53 -07:00
Akash Mozumdar
90b58da867 move file parsing to load time 2022-01-01 22:05:53 -07:00
Blu3train
2316f59415 Added global modifier option 2022-01-01 22:05:53 -07:00
Blu3train
06b93e3def Added ignore case modifier option 2022-01-01 22:05:53 -07:00
Blu3train
fde588d922 Corrected text translation 2022-01-01 22:05:53 -07:00
Blu3train
8e0e17c59b Regex Replacer Extension 2022-01-01 22:05:53 -07:00
Blu3train
e6ee5ecf3d Regex Replacer Extension 2022-01-01 22:05:53 -07:00
k1mlka
49133974b3 add hook artemis3 2022-01-01 22:05:34 -07:00
Akash Mozumdar
4b67b8a5cf make logic clearer, remove second call to size 2022-01-01 22:05:21 -07:00
Blu3train
663bb97fff fix rateLimitTimespan issue #665 2022-01-01 22:05:21 -07:00
Akash Mozumdar
953a537cb8 squashing final bugs with resizing 2022-01-01 22:05:07 -07:00
Akash Mozumdar
8ac4a072a9 bug bashing 2022-01-01 22:05:07 -07:00
Akash Mozumdar
45279d26dc consolidate features and add context menu to edge 2022-01-01 22:05:07 -07:00
Blu3train
5df90e2c01 Added all the features developed by the last PR 2022-01-01 22:05:07 -07:00
Akash Mozumdar
0346f95707 fix x64 build 2022-01-01 22:05:07 -07:00
Akash Mozumdar
1f19a9e74d use system global hotkey 2022-01-01 22:05:07 -07:00
Blu3train
b236a50a8f Fixed bug in calculating height in autoresize mode 2022-01-01 22:05:07 -07:00
Blu3train
2f08ab1e9b added Auto Resize and Click Through features 2022-01-01 22:05:07 -07:00
Akash Mozumdar
ad3f09d8ef bing api update 2022-01-01 21:58:04 -07:00
Akash Mozumdar
44c8e4f94c be more careful inserting waffle 2022-01-01 21:29:13 -07:00
Akash Mozumdar
a17d8d5993 bundle unicode font 2021-11-28 04:57:16 -07:00
Akash Mozumdar
bf49f0bca6 rename 2021-11-13 14:08:16 -07:00
Akash Mozumdar
15db478e62 rearrange build for less confusion and to build CLI faster 2021-11-13 13:38:32 -07:00
Akash Mozumdar
46bc6ec84b add emails 2021-11-08 07:24:22 -07:00
Akash Mozumdar
b3b665fb65 fix debug build 2021-11-08 07:07:34 -07:00
Akash Mozumdar
17b5884149 add basic upstream upgrades 2021-11-08 06:41:28 -07:00
Akash Mozumdar
742b7cacf3 update docs 2021-11-08 06:21:17 -07:00
Akash Mozumdar
4ff150b674 update docs 2021-11-07 06:18:42 -07:00
Akash Mozumdar
e83579ed7c hopefully remove dependency on vcredist for good 2021-11-07 05:54:34 -07:00
Akash Mozumdar
febff243d3 fix bing translate 2021-11-06 17:48:45 -06:00
Akash Mozumdar
1a29f5670a fix crash 2021-11-06 04:53:31 -06:00
Akash Mozumdar
6ed3d9c951 update path 2021-10-23 00:20:44 -06:00
Akash Mozumdar
492c843e5c update qt version to one supported by appveyor 2021-10-23 00:15:05 -06:00
Akash Mozumdar
574eeec79e deepl bugfix 2021-09-06 03:51:51 -06:00
Akash Mozumdar
6c74df864a update deepl devtools 2021-09-06 02:04:25 -06:00
Akash Mozumdar
ca93120442 better search in old horkeye 2021-09-06 02:03:22 -06:00
Akash Mozumdar
eddf1bf0ee ui improvements 2021-09-06 02:02:36 -06:00
Akash Mozumdar
c9ad8880fb fix garbage filter bug 2021-08-18 00:41:34 -06:00
Akash Mozumdar
fb9fb5d54a add universal links to thread linker 2021-08-18 00:22:23 -06:00
Akash Mozumdar
085bec6d5b always open to root 2021-08-17 23:08:42 -06:00
Akash Mozumdar
695672ce9c support larger sentences by default, possibly with repetition (since builtin filter is disabled by default) 2021-08-17 23:08:22 -06:00
Akash Mozumdar
bda08d500e fix minor bugs with rate limiter, update urls 2021-08-17 22:48:16 -06:00
Akash Mozumdar
5862bf50ac small fixes 2021-07-01 23:50:47 -06:00
Akash Mozumdar
493e80e568 change file name and remove global to fix threading issue 2021-07-01 23:45:01 -06:00
Blu3train
db31e19997 cache filename extension translators: added target language name 2021-07-01 23:45:01 -06:00
Akash Mozumdar
d64a2c05b4 get rid of gettickcount 2021-07-01 13:35:33 -06:00
Akash Mozumdar
f8874bf8a0 add rpg maker mz hook 2021-06-30 20:59:07 -06:00
Akash Mozumdar
b28b68d218 upgrade character deduplication algorithm 2021-06-30 20:58:40 -06:00
Akash Mozumdar
50f5b183f1 fix sorting of names 2021-06-30 20:58:04 -06:00
Akash Mozumdar
a566818ad4 improve ui for rate limiter 2021-06-30 19:14:49 -06:00
Akash Mozumdar
ac95d873f1 improve translation extension ui, add language detection for papago and systran, add more languages to systran, improve deepl translation and bing error handling 2021-06-28 22:24:59 -06:00
Akash Mozumdar
444974ee8a improve error handling and fix build 2021-06-07 00:05:45 -06:00
Akash Mozumdar
3b948c3359 update credits 2021-06-06 22:43:49 -06:00
Akash Mozumdar
1eaa054b33 improve devtools resource management and style fixes 2021-06-06 22:43:40 -06:00
Akash Mozumdar
647058e9aa
Merge pull request #568 from Blu3train/devtools_papago_translate
DevTools Papago Translate
2021-06-06 03:25:50 -06:00
Akash Mozumdar
918c877e04
Merge branch 'master' into devtools_papago_translate 2021-06-06 03:15:33 -06:00
Akash Mozumdar
2cf146652d integrate with devtools refactor and remove unneeded code 2021-06-06 03:13:45 -06:00
Blu3train
89198fd4fe Updated CMakeLists.txt and deploy.ps1 files 2021-06-06 03:13:45 -06:00
Blu3train
9e0c95be98 Updated CMakeLists.txt and deploy.ps1 files 2021-06-06 03:13:45 -06:00
Blu3train
7425ae770f DevTools Systran Translate 2021-06-06 03:13:45 -06:00
Akash Mozumdar
e0981f89e8 add new runtime file 2021-06-05 17:16:21 -06:00
Akash Mozumdar
eb1421c143 fix style issues and use blank icons 2021-06-05 17:16:21 -06:00
tera8m4
a1d3abb080 Create a new attach dialog window
New dialog window that shows processes with icons in QListView instead of ComboBox.
2021-06-05 17:16:21 -06:00
tera8m4
dd4b8cfbb5 Create small helpers to get icon from executable 2021-06-05 17:16:21 -06:00
Akash Mozumdar
615d372eeb missing vcredist causes some weird stuff... 2021-06-05 10:11:36 -06:00
Akash Mozumdar
e4cea83ae4 integrate with devtools refactor and remove unneeded code 2021-06-05 09:43:53 -06:00
Akash Mozumdar
9e26c462e7 Merge remote-tracking branch 'origin/master' into pr/568-devtools-papago-translate 2021-06-05 09:41:33 -06:00
Akash Mozumdar
0df2526560 fix botched merge 2021-06-05 08:50:00 -06:00
Akash Mozumdar
de3cad37f8
Merge pull request #578 from Blu3train/devtools_fixed_failure_remove_devtoolscache
devtools: fixed failure to remove devtoolscache when closing the program
2021-06-05 08:31:46 -06:00
Akash Mozumdar
8c9faf9947
Merge branch 'master' into devtools_fixed_failure_remove_devtoolscache 2021-06-05 08:31:04 -06:00
Akash Mozumdar
76fea31fb3 fix bugs and add new languages to deepl 2021-06-05 08:25:46 -06:00
Akash Mozumdar
795ecce45e fix bing trranslate 2021-06-05 08:15:19 -06:00
Akash Mozumdar
675695cde1 prevent write starvation 2021-06-05 03:00:01 -06:00
Akash Mozumdar
9f8e523ce3 (hopefully) improved renpy hook 2021-06-05 02:30:34 -06:00
Akash Mozumdar
d74dcdc286 fix thread linker crash and other minor improvements 2021-06-05 02:20:04 -06:00
Akash Mozumdar
b4aa113fac turn off repetiton filter by default, use google by default, increase max hook count, replace regex with capture 2021-06-05 01:48:10 -06:00
Blu3train
1e03e69d00 devtools: fixed failure to remove devtoolscache when closing the browser 2021-05-26 00:41:19 +02:00
Blu3train
76d716956d DevTools Papago Translate 2021-05-15 11:07:22 +02:00
Akash Mozumdar
1782292662 fix wrong output for non utf text 2021-04-28 21:02:31 -06:00
Akash Mozumdar
3c33d11d80 fix function finding (restore to ithvnr) - should fix system40 and rugp2 hooks 2021-04-28 21:01:28 -06:00
Akash Mozumdar
a8ae2156d9 fix build errors 2021-04-24 17:12:40 -06:00
Akash Mozumdar
f26f30dce7 small fixes 2021-04-24 16:45:17 -06:00
MishaIac
1fcacc7bc9 Update text.cpp 2021-04-24 16:40:11 -06:00
Akash Mozumdar
71fe1410c2 fix error handling 2021-04-24 16:39:52 -06:00
Akash Mozumdar
aa0c0e0047 build system improvements 2021-03-13 00:51:36 -07:00
Akash Mozumdar
74121d7484 doc fixes 2021-03-12 08:23:41 -07:00
Akash Mozumdar
8543d49192 fix google translate and devtools deepl translate, add chrome file selector, add garbage filter 2021-03-09 21:32:56 -07:00
Akash Mozumdar
5537679442 update credits 2021-03-08 08:48:37 -07:00
Akash Mozumdar
20f35a02ed improve extension UI 2021-03-08 08:46:34 -07:00
Akash Mozumdar
1bab6956a8 improve renpy detection 2021-03-08 08:41:50 -07:00
Akash Mozumdar
acc85f3a86 minor improvements 2021-03-08 08:41:34 -07:00
Akash Mozumdar
54a285b53b improve hook search ux 2021-03-08 08:37:02 -07:00
Akash Mozumdar
bf97055155 small fixes 2021-02-21 14:15:59 -07:00
Akash Mozumdar
ad83eb290a update docs 2021-01-31 08:02:10 -07:00
Akash Mozumdar
c7ae06c121 final? cleanup 2021-01-30 23:43:25 -07:00
Akash Mozumdar
fdc53a393d Appropriate name 2021-01-30 23:17:44 -07:00
Akash Mozumdar
68b382e5fa upload alpha releases (with debug info) 2021-01-30 23:01:32 -07:00
Akash Mozumdar
9e6f638488 integrate new extensions 2021-01-30 16:42:29 -07:00
Akash Mozumdar
fcd735283d temporary remove regex replacement 2021-01-30 13:11:46 -07:00
Akash Mozumdar
a03c939043 fix small bugs 2021-01-30 12:45:37 -07:00
Akash Mozumdar
faa14f8cb9 fix crash (don't use avx instructions) 2021-01-22 09:11:35 -07:00
Akash Mozumdar
e00b831e3d fix localization bug, add opption to select language translating from, and add error message to devtools deepl 2021-01-21 07:07:49 -07:00
Akash Mozumdar
084f26a72d allow replacement in regex filter and make names consistent 2021-01-21 07:07:49 -07:00
lgztx96
4219115a40
update waffle hook (#461)
* update waffle hook
2021-01-16 22:02:46 -07:00
Akash Mozumdar
43aa1d6711 set up appveyor and improve qt version finding 2021-01-15 21:39:38 -07:00
Akash Mozumdar
662ca45321 add styler extension 2021-01-15 09:53:57 -07:00
Akash Mozumdar
dcff7e550c
Merge pull request #416 from zeheyler/devtools_deepl_extension
add devtools api and new deepl extension
2021-01-15 09:35:03 -07:00
Akash Mozumdar
c62c586a59 fix bugs and autoload lua 2021-01-15 09:32:23 -07:00
Akash Mozumdar
fc81b17a3c massive refactor, also fix newline issue and google translate throw and different number formats for process id 2021-01-15 06:07:23 -07:00
Akash Mozumdar
3a34f989e5 Merge branch 'master' into pr/416-add-devtools-api-and-new-deepl-extension 2020-12-16 12:23:52 -07:00
Akash Mozumdar
2c9ac1da3c improve JSON parser 2020-12-15 07:28:12 -07:00
Akash Mozumdar
95b145bece revert separation line 2020-12-14 19:10:47 -07:00
Akash Mozumdar
457aed96c2 fix unicode mangling 2020-12-14 10:37:54 -07:00
Akash Mozumdar
4783469a0a style 2020-12-14 07:48:02 -07:00
Akash Mozumdar
0680de413a update credits 2020-12-14 06:34:28 -07:00
k1mlka
29d23935ad versatile way 2020-12-14 06:26:51 -07:00
k1mlka
3aee03a244 update specific game hook 2020-12-14 06:26:51 -07:00
Akash Mozumdar
268f40771d improve reliability of translations 2020-12-14 06:26:01 -07:00
zeheyler
7eb45899c0 fix target language check
Fixed target language check
2020-12-13 00:56:00 +03:00
zeheyler
ea6ddc7a7c small improvements
Added check for the empty answer from the translator
Small refactors
2020-12-09 14:00:57 +03:00
zeheyler
6958b22e68 fix target selector check
Fixed target selector check in some cases
Changed line endings from CRLF to LF
2020-11-04 20:51:23 +03:00
zeheyler
9bb7fbff06 add source language selection and rename variables
Added source language selection since autodetect fails sometimes
Refactored names of variables
2020-11-04 01:23:20 +03:00
Akash Mozumdar
1b71b3ee86 update links 2020-11-02 06:58:44 -07:00
Akash Mozumdar
1634120669 give credit 2020-11-02 06:44:46 -07:00
Gratusfr
f8b4bc1ae3 ortho + partial translation of CODE_INFODUMP 2020-11-02 06:39:16 -07:00
Akash Mozumdar
01b6b6de16 fix things not being localized and settings not loading 2020-11-02 06:27:21 -07:00
zeheyler
4b46057800 add backup check method
Added backup check method in case the first method fails
2020-10-29 02:08:11 +03:00
zeheyler
93b8711bf8 refactor errorcode
Refactored error processing
2020-10-22 23:15:45 +03:00
zeheyler
8ab1e576bd add call queue check
Added queue check for multiple calls from the main program
2020-10-21 00:10:58 +03:00
zeheyler
3af4217075 add check for outdated doc and refactor input text modifications
Added the flag for checking if webdocument structure is outdated
Refactor text modifications before processing to the translator
2020-10-20 02:08:59 +03:00
zeheyler
2ab780a491 add user-agent switch method
Add method that changes user-agent when in the headless mode
2020-10-18 16:16:06 +03:00
zeheyler
73d1f21bc1 add notification alert
When timeout from the net the method searches for notifications from the translator and put them to output
2020-10-15 23:23:41 +03:00
zeheyler
a3ebaf0023 add event catch method and refactor error messages
Added method that catches specified events from the page
Refactored error messages
2020-10-15 02:32:58 +03:00
Akash Mozumdar
f0a1690c92 fix deepl 2020-10-13 20:19:53 -06:00
zeheyler
6b54ec0733 add devtools api and new deepl extension
Added the DevTools API and the wrapper with  the use of the QtWebSockets library.
Added a new DeepL extension which uses the DevTools API.
2020-10-14 03:37:03 +03:00
Akash Mozumdar
fe1cdfc947 custom filetype for extensions 2020-09-10 07:18:17 -06:00
Akash Mozumdar
f6742de3d5 parse shortened hook codes 2020-09-10 07:12:50 -06:00
Akash Mozumdar
dc1f819952 fix infinite loop 2020-09-09 15:13:19 -06:00
Akash Mozumdar
8e67827ae6 update anex86 hook 2020-09-09 14:58:19 -06:00
Akash Mozumdar
93238e13ed fix line endings 2020-08-12 09:31:48 -06:00
StarFang208
ef9d05f10e Updated text.cpp
Added the untranslated lines
2020-08-12 09:31:48 -06:00
Akash Mozumdar
b4303e4d4d refactor and add error handling 2020-08-12 03:31:46 -06:00
Jazzinghen
39b0882bbf Look for start of function with memory functions 2020-08-12 03:31:46 -06:00
Jazzinghen
79058c6811 Added Database extractor hook 2020-08-12 03:31:46 -06:00
Jazzinghen
681fc5b628 Updated search code to look for less memory 2020-08-12 03:31:46 -06:00
Jazzinghen
cc7d02d8a1 Removed useless import that I used for debugging 2020-08-12 03:31:46 -06:00
Jazzinghen
409ad121ae Removed debug message 2020-08-12 03:31:46 -06:00
Jazzinghen
ae35f82199 Completed the hook. 2020-08-12 03:31:46 -06:00
Jazzinghen
51b217169e Finally have something that runs and crashes.
This thing is pointing at a wrong memory location due to the fact that
there's another function that STARTS THE SAME.

I'll use another method to do this.
2020-08-12 03:31:46 -06:00
Jazzinghen
261da66d74 Adding TokyoNecro matching function 2020-08-12 03:31:46 -06:00
Akash Mozumdar
fe3e152553 steal updated chinese translation 2020-08-12 02:27:03 -06:00
Akash Mozumdar
341b6915c8 steal some hooks for willplus and waffle 2020-08-12 02:18:39 -06:00
Akash Mozumdar
6da63208c7 try both deepl api versions 2020-08-12 01:42:24 -06:00
Akash Mozumdar
9c006bce17 ignore default config file 2020-08-12 01:41:13 -06:00
Akash Mozumdar
c3c73d2ac2 fix french translation 2020-07-02 13:06:24 -06:00
Akash Mozumdar
ae5fa5fb9f
Fix link 2020-05-16 20:43:07 -06:00
Akash Mozumdar
d86f22877c document 2020-05-16 07:11:49 -06:00
Akash Mozumdar
287776903a add french readme 2020-05-16 03:00:50 -06:00
Akash Mozumdar
166ec1456b add dummy readme 2020-05-14 18:39:13 -06:00
Akash Mozumdar
5739af5a3f add french translation 2020-05-14 18:35:26 -06:00
Akash Mozumdar
4b0fc12794 dynamic port 2020-05-14 18:09:52 -06:00
TokcDK
3e21a6b4ba Updated Russian localization 2020-05-14 18:09:42 -06:00
Akash Mozumdar
a2d1374ad4 get rid of donate buttons (i'm doing great, please give the money to those who need it) 2020-04-25 22:01:52 -06:00
Akash Mozumdar
a129394fb5 dont need that param anymore 2020-04-25 20:39:12 -06:00
Akash Mozumdar
e20a12862c default setting tweak 2020-04-25 20:36:58 -06:00
Akash Mozumdar
9711ce94c9 thread linker doesn't append sentences anymore 2020-04-25 20:34:53 -06:00
Akash Mozumdar
535dac480a error when failed to create config file 2020-04-25 20:07:42 -06:00
Akash Mozumdar
d1887db2a5 use precompiled headers 2020-04-18 19:04:07 -06:00
Akash Mozumdar
386e09a81a fix translation extensions 2020-04-02 03:07:32 -06:00
Akash Mozumdar
4a320a3659 use bottom 16 bits of ctx (deals with aslr) 2020-03-30 13:19:35 -06:00
Akash Mozumdar
26960cf099 autofill regex 2020-03-29 20:55:25 -06:00
Akash Mozumdar
86415fca10 all official apis now supported and performance improvements 2020-03-29 20:55:12 -06:00
Akash Mozumdar
ecab473482 small fixes 2020-03-27 05:55:11 -06:00
Akash Mozumdar
accd231c64 add option to only translate selected thread 2020-03-27 04:07:05 -06:00
Akash Mozumdar
8a710659dc merge google and google cloud 2020-03-26 06:12:20 -06:00
Akash Mozumdar
87c056a5b3 add settings to translation dialog and implement deepl 2020-03-26 06:05:42 -06:00
Akash Mozumdar
19f35f743c inject correctly in CLI 2020-03-26 05:39:25 -06:00
Akash Mozumdar
bb0df71da2 suppress mono_* when configured 2020-03-26 03:13:21 -06:00
Akash Mozumdar
af2ba72f39 fix stack overflow 2020-03-23 19:37:11 -06:00
Akash Mozumdar
402b685820 sign all at once 2020-03-17 22:09:20 -06:00
Akash Mozumdar
e4d9b70040 better logging for translation extensions 2020-03-17 22:01:16 -06:00
Akash Mozumdar
bc01179626 multiple mono hooks and fix crash 2020-03-17 13:53:46 -06:00
Akash Mozumdar
d1d453f359 remove weird stuff 2020-03-16 03:04:40 -06:00
Akash Mozumdar
ac4cec9708 add gcp translator and add continous dialog for translators and update language lists and make extension dialogs uncloseable without properly removing the extension 2020-03-16 02:56:04 -06:00
Akash Mozumdar
df3d3a0355 sign all files 2020-03-16 02:51:45 -06:00
Akash Mozumdar
39e81bb904 escape unformatted 2020-03-07 04:06:45 -07:00
Akash Mozumdar
d6412ca0b0 fix lookup with << 2020-03-07 03:45:26 -07:00
Akash Mozumdar
b9081f2472 fix large file being read wrong 2020-03-07 03:45:07 -07:00
Akash Mozumdar
a0306fea8b remove all nulls 2020-03-05 02:23:53 -07:00
Akash Mozumdar
2be920c50e cleaner output for mono 2020-03-05 02:19:04 -07:00
Akash Mozumdar
3ff31e0ac6 reorganize config files 2020-03-05 01:51:36 -07:00
Akash Mozumdar
aa4b980422 fix build 2020-03-05 01:18:12 -07:00
Akash Mozumdar
5fb78b87c7 easily configure games and save preference for jp locale 2020-03-05 01:17:45 -07:00
Akash Mozumdar
99be5fd40f configurable max size 2020-03-04 23:30:59 -07:00
Akash Mozumdar
6bd0c74c83 fix build 2020-03-03 00:13:15 -07:00
Akash Mozumdar
b4b26d50ca attach to current process 2020-03-02 23:49:31 -07:00
Akash Mozumdar
b515a17767 fix exception 2020-03-02 23:49:18 -07:00
Akash Mozumdar
6a61342d19 fix issue with too large sentence and topmost window blocking dictionary 2020-03-02 23:48:29 -07:00
Akash Mozumdar
fcb525df36 yeah...that was never a class. improve performance and add current process function 2020-03-02 23:38:51 -07:00
Akash Mozumdar
49432f689d (hopefully) fix silent crashes 2020-03-01 23:41:27 -07:00
Akash Mozumdar
74b4ae9da5 build tests 2020-03-01 06:10:06 -07:00
Akash Mozumdar
47836f20f3 oops, spdefault uses address bounds 2020-03-01 06:04:22 -07:00
Akash Mozumdar
360254d5bf pcsx2 search 2020-03-01 05:42:34 -07:00
Akash Mozumdar
b7978ff512 detect precompiled unity 2020-03-01 04:56:20 -07:00
Akash Mozumdar
0d4af5c140 fix tinkerbell and add getstringtype and foldstring 2020-02-29 18:32:58 -07:00
Akash Mozumdar
ffff4f2eb9 tiny fixes 2020-02-29 17:51:38 -07:00
Akash Mozumdar
0688daf1da fiix build 2020-02-29 05:33:14 -07:00
Akash Mozumdar
a490ebc274 add italian to readmes 2020-02-29 01:42:39 -07:00
StarFang208
a40ae621af
Italian Readme and Text translation (
* Italian Readme and Text file

Added the text.cpp wit the italian lines, as well the italian README.

* update from old fork point

* fix line endings

Co-authored-by: Akash Mozumdar <akashmozumdar@gmail.com>
2020-02-28 05:13:59 -07:00
Akash Mozumdar
b79af59e3d use qregularexpression as hook search filter 2020-02-28 04:21:07 -07:00
Akash Mozumdar
c2725e8518 add BOM 2020-02-28 03:17:41 -07:00
Akash Mozumdar
22e0d38a1e small fixes 2020-02-28 02:34:07 -07:00
Akash Mozumdar
f6cf3c9c62 save regex 2020-02-28 02:33:51 -07:00
Akash Mozumdar
16540bfe69 move utility functions into common.h and module.h 2020-02-28 00:34:34 -07:00
Akash Mozumdar
200c01f4ab more reliable default font and only load window position if on screen 2020-02-28 00:12:23 -07:00
Akash Mozumdar
d6b0114f27 using newer qt 2020-02-27 05:33:22 -07:00
Akash Mozumdar
73ccb38641 why did i ever think that was a good idea? 2020-02-27 04:42:29 -07:00
Akash Mozumdar
c8853a1af8 dont remove mono if flooding, instead use config file for which hook to insert 2020-02-27 04:26:01 -07:00
Akash Mozumdar
565f99cced implement deinflection 2020-02-26 01:01:06 -07:00
Akash Mozumdar
dc48f2a3c8 performance improvements and autoselect native language for translations 2020-02-25 04:39:27 -07:00
Akash Mozumdar
2e23d4016d better position for dictionary 2020-02-23 00:44:26 -07:00
Akash Mozumdar
2d2a3dedb9 make dictionary mouseover and make extra window larger to start 2020-02-22 20:58:47 -07:00
Akash Mozumdar
b61272a5e6 implement replacer wildcard and optimize memory usage when loading files 2020-02-16 17:58:09 -07:00
106 changed files with 5324 additions and 2034 deletions

39
.appveyor.yml Normal file
View 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
View File

@ -0,0 +1,3 @@
[submodule "texthook/minhook"]
path = texthook/minhook
url = https://github.com/TsudaKageyu/minhook.git

View File

@ -1,6 +1,11 @@
cmake_minimum_required(VERSION 3.5)
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,28 +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) # uncomment to build CLI
endif()

View File

@ -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" ]
}
]
}
}

View File

@ -1,26 +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
- 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!

View File

@ -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/util.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()

View 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
View 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;
};

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

View File

@ -1,11 +1,11 @@
#include "util.h"
#include "module.h"
#include <sstream>
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"; }
@ -21,8 +21,11 @@ namespace
__declspec(noreturn) void Terminate()
{
MessageBoxW(NULL, lastError.c_str(), L"Textractor ERROR", MB_ICONERROR);
abort();
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), INFINITE);
}
LONG WINAPI ExceptionLogger(EXCEPTION_POINTERS* exception)
@ -36,7 +39,7 @@ namespace
errorMsg << std::uppercase << std::hex <<
L"Error code: " << exception->ExceptionRecord->ExceptionCode << std::endl <<
L"Error address: " << exception->ExceptionRecord->ExceptionAddress << std::endl <<
L"Error in module: " << Util::GetModuleFilename((HMODULE)info.AllocationBase).value_or(L"Could not find") << std::endl <<
L"Error in module: " << GetModuleFilename((HMODULE)info.AllocationBase).value_or(L"Could not find") << std::endl <<
L"Additional info: " << info.AllocationBase << std::endl;
if (exception->ExceptionRecord->ExceptionCode == 0xE06D7363)

View File

@ -1,7 +1,5 @@
#include "extenwindow.h"
#include "ui_extenwindow.h"
#include "defs.h"
#include <concrt.h>
#include <QMenu>
#include <QFileDialog>
#include <QDragEnterEvent>
@ -11,34 +9,41 @@
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;
extern const char* EXTEN_WINDOW_INSTRUCTIONS;
constexpr auto DEFAULT_EXTENSIONS = u8"Remove Repeated Characters>Remove Repeated Phrases>Regex Filter>Copy to Clipboard>Bing Translate>Extra Window>Extra Newlines";
namespace
{
constexpr auto EXTEN_SAVE_FILE = u8"SavedExtensions.txt";
constexpr auto DEFAULT_EXTENSIONS = u8"Remove Repeated Characters>Regex Filter>Copy to Clipboard>Google Translate>Extra Window>Extra Newlines";
struct Extension
{
std::wstring name;
wchar_t* (*callback)(wchar_t*, const InfoForExtension*);
};
Ui::ExtenWindow ui;
concurrency::reader_writer_lock extenMutex;
std::vector<Extension> extensions;
ExtenWindow* This = nullptr;
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;
}
@ -50,19 +55,62 @@ 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); }));
::extensions = extensions;
}
void Sync()
{
ui.extenList->clear();
QTextFile extenSaveFile(EXTEN_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate);
concurrency::reader_writer_lock::scoped_lock_read readLock(extenMutex);
for (auto extension : extensions)
{
ui.extenList->addItem(S(extension.name));
extenSaveFile.write((S(extension.name) + ">").toUtf8());
}
}
void Add(QFileInfo extenFile)
{
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.fileName())) return Sync();
}
QMessageBox::information(This, EXTENSIONS, QString(INVALID_EXTENSION).arg(extenFile.fileName()));
}
void Delete()
{
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);
}
}
bool DispatchSentenceToExtensions(std::wstring& sentence, const InfoForExtension* sentenceInfo)
@ -71,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();
@ -79,71 +127,33 @@ 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),
ui(new Ui::ExtenWindow)
ExtenWindow::ExtenWindow(QWidget* parent) : QMainWindow(parent, Qt::WindowCloseButtonHint)
{
ui->setupUi(this);
connect(ui->extenList, &QListWidget::customContextMenuRequested, [this](QPoint point)
{
if (QMenu(this).exec({ std::make_unique<QAction>(ADD_EXTENSION).get() }, ui->extenList->mapToGlobal(point)))
if (QString extenFile = QFileDialog::getOpenFileName(this, ADD_EXTENSION, ".", EXTENSIONS + QString(" (*.dll)")); !extenFile.isEmpty()) Add(extenFile);
});
ui->vboxLayout->addWidget(new QLabel(EXTEN_WINDOW_INSTRUCTIONS, this));
This = this;
ui.setupUi(this);
ui.vboxLayout->addWidget(new QLabel(EXTEN_WINDOW_INSTRUCTIONS, this));
setWindowTitle(EXTENSIONS);
ui->extenList->installEventFilter(this);
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);
for (auto extenName : QString(QTextFile(EXTEN_SAVE_FILE, QIODevice::ReadOnly).readAll()).split(">")) Load(extenName);
Sync();
}
ExtenWindow::~ExtenWindow()
{
delete ui;
}
void ExtenWindow::Add(QFileInfo extenFile)
{
if (extenFile.suffix() == "dll")
{
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();
}
QMessageBox::information(this, EXTENSIONS, QString(INVALID_EXTENSION).arg(extenFile.fileName()));
}
void ExtenWindow::Sync()
{
ui->extenList->clear();
QTextFile extenSaveFile(EXTEN_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate);
concurrency::reader_writer_lock::scoped_lock_read readLock(extenMutex);
for (auto extension : extensions)
{
ui->extenList->addItem(S(extension.name));
extenSaveFile.write((S(extension.name) + ">").toUtf8());
}
}
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;
for (int i = 0; i < ui->extenList->count(); ++i) extenNames.push_back(ui->extenList->item(i)->text());
for (int i = 0; i < ui.extenList->count(); ++i) extenNames.push_back(ui.extenList->item(i)->text());
Reorder(extenNames);
Sync();
}
@ -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)

View File

@ -2,11 +2,6 @@
#include "qtcommon.h"
namespace Ui
{
class ExtenWindow;
}
struct InfoForExtension
{
const char* name;
@ -20,17 +15,10 @@ class ExtenWindow : public QMainWindow
{
public:
explicit ExtenWindow(QWidget* parent = nullptr);
~ExtenWindow();
private:
inline static constexpr auto EXTEN_SAVE_FILE = u8"SavedExtensions.txt";
void Add(QFileInfo extenFile);
void Sync();
bool eventFilter(QObject* target, QEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override;
Ui::ExtenWindow* ui;
};

View File

@ -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
util.cpp
)

View File

@ -1,15 +0,0 @@
#pragma once
#include "common.h"
#include "types.h"
namespace Util
{
std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL);
std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL);
std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses();
std::optional<std::wstring> GetClipboardText();
std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8);
std::optional<HookParam> ParseCode(std::wstring code);
std::wstring GenerateCode(HookParam hp, DWORD processId = 0);
}

View File

@ -1,7 +1,6 @@
#include "mainwindow.h"
#include "host/util.h"
#include "module.h"
#include <winhttp.h>
#include <QApplication>
extern const wchar_t* UPDATE_AVAILABLE;
@ -24,7 +23,7 @@ int main(int argc, char *argv[])
}
}).detach();
QDir::setCurrent(QFileInfo(S(Util::GetModuleFilename().value())).absolutePath());
QDir::setCurrent(QFileInfo(S(GetModuleFilename().value())).absolutePath());
QApplication app(argc, argv);
app.setFont(QFont("MS Shell Dlg 2", 10));

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +1,12 @@
#pragma once
#include "qtcommon.h"
#include "extenwindow.h"
#include "host/host.h"
namespace Ui
{
class MainWindow;
}
class MainWindow : public QMainWindow
{
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
inline static constexpr auto HOOK_SAVE_FILE = u8"SavedHooks.txt";
inline static constexpr auto GAME_SAVE_FILE = u8"SavedGames.txt";
void closeEvent(QCloseEvent*) override;
void ProcessConnected(DWORD processId);
void ProcessDisconnected(DWORD processId);
void ThreadAdded(TextThread& thread);
void ThreadRemoved(TextThread& thread);
bool SentenceReceived(TextThread& thread, std::wstring& sentence);
void OutputContextMenu(QPoint point);
QString TextThreadString(TextThread& thread);
ThreadParam ParseTextThreadString(QString ttString);
DWORD GetSelectedProcessId();
std::array<InfoForExtension, 10> GetSentenceInfo(TextThread& thread);
std::optional<std::wstring> UserSelectedProcess();
void AttachProcess();
void LaunchProcess();
void DetachProcess();
void ForgetProcess();
void AddHook();
void AddHook(QString hook);
void RemoveHooks();
void SaveHooks();
void FindHooks();
void Settings();
void Extensions();
void ViewThread(int index);
void SetOutputFont(QString font);
Ui::MainWindow* ui;
ExtenWindow* extenWindow;
std::unordered_set<DWORD> alreadyAttached;
bool autoAttach = false, autoAttachSavedOnly = true;
bool showSystemProcesses = false;
uint64_t savedThreadCtx = 0, savedThreadCtx2 = 0;
wchar_t savedThreadCode[1000] = {};
TextThread* current = nullptr;
void closeEvent(QCloseEvent*);
};

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<width>1000</width>
<height>600</height>
</rect>
</property>
@ -77,8 +77,8 @@
<widget class="QPlainTextEdit" name="textOutput">
<property name="font">
<font>
<family>Meiryo</family>
<pointsize>12</pointsize>
<family>Arial Unicode MS</family>
<pointsize>13</pointsize>
</font>
</property>
<property name="contextMenuPolicy">

Binary file not shown.

View File

@ -2,30 +2,28 @@
![How it looks](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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.11<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
View 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)

View File

@ -1,12 +1,12 @@
# Textractor
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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
View 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)

View File

@ -2,18 +2,15 @@
![How it looks](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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.11<br>
Lalu kamu dapat membuka folder di Visual Studio, dan build. Run Textractor.exe.
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. 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)

52
README_IT.md Normal file
View File

@ -0,0 +1,52 @@
# Textractor
![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) ● [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](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).
## Caratteristiche
- Altamente estensibile e personalizzabile
- Aggancia automaticamente molti engine di gioco (inclusi alcuni non supportati da VNR!)
- Aggancia il testo utilizzando codici /H "hook" (molti codici AGTH supportati)
- Estrae il testo direttamente usando codici /R "read"
## Supporto
Fatemi sapere su qualunque bug, giochi che Textractor ha problemi nell'agganciare, richieste future, o altri suggerimenti.<br>
Se avete dei problemi nel agganciare un gioco vi prego di inviarmi via email un sito dove posso scaricarlo, o regalatemelo su [Steam](https://steamcommunity.com/profiles/76561198097566313/).
## Estenzioni
Guardate il mio [Progetto Example Extension](https://github.com/Artikash/ExampleExtension) per vedere come costruire un estenzione..<br>
Guardate la cartella delle estenzioni per esempi di cosa possono fare le estenzioni.
## Contributi
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](text.cpp) cosi come questo README.
## Compiling
Prima di compilare *Textractor*, dovresti ottenere Visual Studio con supporto CMAKE, cosi come Qt versione 5.13<br>
Dovresti essere in grado di aprire la cartella in Visual Studio, e costruire. Avvia Textractor.exe
## Architettura del progetto
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](docs/CREDITS.md)

View File

@ -1,6 +1,6 @@
# Textractor
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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** はビジュアルノベル文字抽出プログラム。

View File

@ -2,18 +2,15 @@
![How it looks](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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)서 받을 수 있습니다.
## 특징
@ -38,16 +35,16 @@ Textractor 실행오류를 겪는다면 vcredist를 실행해 보시기 바랍
## 컴파일링
*Textractor*를 컴파일링 하기 전에, Qt version 5.11과 CMake를 포함한 Visual Studio가 있어야 합니다.<br>
*Textractor*를 컴파일링 하기 전에, Qt version 5.13과 CMake를 포함한 Visual Studio가 있어야 합니다.<br>
그 이후로는, 단순히 Visual Studio를 통해 폴더를 열고 빌드하는 것으로 실행이 가능합니다.
## 프로젝트 아키텍쳐
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)

View File

@ -2,18 +2,15 @@
![Como se Parece](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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,22 +31,22 @@ 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
Antes de compilar o *Textractor* você deve ter o Visual Studio com suporte ao CMake, assim como o Qt versão 5.11.<br>
Antes de compilar o *Textractor* você deve ter o Visual Studio com suporte ao CMake, assim como o Qt versão 5.13.<br>
Você deverá então ser capaz de simplesmente abrir uma pasta no Visual Studio e build. Inicie Textractor.exe.
## 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)

View File

@ -2,18 +2,15 @@
![Как это выглядит](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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,22 +31,22 @@
## Вклад
Любой вклад приветствуется! Пишите мне(автору)(нет, я не занят!) на 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.
## Компиляция
Перед компиляцией *Textractor*, установите Visual Studio с поддержкой CMake, а также Qt версии 5.11<br>
Перед компиляцией *Textractor*, установите Visual Studio с поддержкой CMake, а также Qt версии 5.13<br>
Тогда вы сможете просто открыть и построить проект в Visual Studio. Запустите Textractor.exe.
## Архитектура проекта
Хост (смотрите папку 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)

View File

@ -1,10 +1,10 @@
# Textractor
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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,22 +28,22 @@ 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 即可.
## 编译
编译 *Textractor* 前, 你应当获取支持 CMake 的 Visual Studio, 以及 Qt 5.11 版.<br>
编译 *Textractor* 前, 你应当获取支持 CMake 的 Visual Studio, 以及 Qt 5.13 版.<br>
之后就可以使用 Visual Studio 打开文件夹, 然后构建. 运行 Textractor.exe.
## 项目架构
宿主 (位于 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)

View File

@ -2,20 +2,16 @@
![How it looks](screenshot.png)
[English](README.md) ● [Español](README_ES.md) ● [简体中文](README_SC.md) ● [日本語](README_JP.md) ● [Русский](README_RU.md) ● [Bahasa](README_ID.md) ● [Português](README_PT.md) ● [ภาษาไทย](README_TH.md) ● [한국어](README_KR.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&currency_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).
## คุณสมบัติ
@ -41,7 +37,7 @@ ITHVNR รุ่นสุดท้ายสามารถดาวน์โห
## โครงสร้างโปรแกรม
ฐานของโปรแกรม (โฟลเดอร์ GUI/host) ส่งข้อมูลจาก texthook.dll (ที่ถูกสร้างจาก texthook โฟลเดอร์) ไปยังเกมเป้าหมาย และ เชื่อมทั่งสองอย่างเข้าด้วยกัน<br>
ฐานของโปรแกรม (โฟลเดอร์ host) ส่งข้อมูลจาก texthook.dll (ที่ถูกสร้างจาก texthook โฟลเดอร์) ไปยังเกมเป้าหมาย และ เชื่อมทั่งสองอย่างเข้าด้วยกัน<br>
ฐานโปรแกรมเขียนผ่านฝั่ง hostPipe(ท่อเชื่อมฝั่งฐานข้อมูล) ในขณะที่ตัวดึงตัวอักษรที่ทางฝั่ง hookPipe(ท่อเชื่อมฝั่งดึงข้อมูล).<br>
ตัวดึงตัวอักษรรอการเชื่อมเข้ากับของทั่งสองท่อ หลังจากนั่นส่งคำสั่งไปยังข้อมูลนั่น (เช่น แสดงผลข้อมูล เป็นต้น) และทำให้ข้อมูลส่งผ่านต่อมาออกมาได้ถูกต้อง<br>
ข้อมูลบางอย่างเกี่ยวกับการเชื่อมจะถูกแลกเปลี่ยนผ่านความทรงจำของระบบ (shared memory)
@ -49,4 +45,4 @@ ITHVNR รุ่นสุดท้ายสามารถดาวน์โห
ตัวอักษรที่ฐานโปรแกรมรับผ่านท่อจะถูกแปลงเล็กน้อยก่อนที่จะแสดงผ่าน GUI <br>
สุดท้ายแล้ว GUI จะส่งข้อมูลตัวอักษรไปยังส่วนขยายต่างๆก่อนที่จะแสดงให้เห็นในหน้าจอ
## [นักพัฒนา](CREDITS.md)
## [นักพัฒนา](docs/CREDITS.md)

View File

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

View File

@ -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;
@ -16,6 +16,8 @@ foreach ($language in @{
PORTUGUESE="Portuguese";
THAI="Thai";
KOREAN="Korean";
ITALIAN="Italian";
FRENCH="French"
}.GetEnumerator())
{
$folder = "Textractor-$($language.Value)-$version";
@ -29,31 +31,41 @@ foreach ($language in @{
&"C:\Program Files\CMake\bin\cmake" -G "Visual Studio 16 2019" -A"$VS_arch" -DVERSION="$version" -DTEXT_LANGUAGE="$($language.Key)" -DCMAKE_BUILD_TYPE="Release" ../..;
&"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv" Textractor.sln /build "Release|$VS_arch";
cd ..;
&"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /v /t "http://timestamp.digicert.com" /fd SHA256 "Release_$arch/Textractor.exe";
&"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /v /t "http://timestamp.digicert.com" /fd SHA256 "Release_$arch/TextractorCLI.exe";
mkdir -Force -Verbose "$folder/$arch";
foreach ($file in @(
"Textractor.exe",
"TextractorCLI.exe",
"texthook.dll",
"Bing Translate.dll",
"Copy to Clipboard.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\**\*");
}
rm -Force -Recurse -Verbose "Runtime";
@ -66,6 +78,9 @@ foreach ($arch in @("x86", "x64"))
"LocaleEmulator.dll",
"Qt5Core.dll",
"Qt5Gui.dll",
"Qt5Network.dll",
"Qt5WebSockets.dll",
"Qt5WinExtras.dll"
"Qt5Widgets.dll",
"platforms",
"styles"
@ -73,12 +88,14 @@ 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/ ../INSTALL_THIS_UNICODE_FONT.ttf
cd ..
cd ..;
&"C:\Program Files (x86)\Inno Setup 6\iscc" -DVERSION="$version" installer.iss;
&"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /v /t "http://timestamp.digicert.com" /fd SHA256 "Builds/Textractor-$version-Setup.exe";

32
docs/CREDITS.md Normal file
View 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
View 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.

View File

@ -1,26 +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()

View File

@ -1,71 +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;
const char* TRANSLATION_PROVIDER = "Bing";
QStringList languages
const char* TRANSLATION_PROVIDER = "Bing Translate";
const char* GET_API_KEY_FROM = "https://www.microsoft.com/en-us/translator/business/trial/#get-started";
extern const QStringList languagesTo
{
"English: en",
"Arabic: ar",
"Bosnian: bs-Latn",
"Bulgarian: bg",
"Catalan: ca",
"Chinese(Simplified): zh-CHS",
"Chinese(Traditional): zh-CHT",
"Croatian: hr",
"Czech: cs",
"Danish: da",
"Dutch: nl",
"Estonian: et",
"Finnish: fi",
"French: fr",
"German: de",
"Greek: el",
"Hebrew: he",
"Hindi: hi",
"Hungarian: hu",
"Indonesian: id",
"Italian: it",
"Japanese: ja",
"Klingon: tlh",
"Korean: ko",
"Latvian: lv",
"Lithuanian: lt",
"Malay: ms",
"Maltese: mt",
"Norwegian: no",
"Persian: fa",
"Polish: pl",
"Portuguese: pt",
"Romanian: ro",
"Russian: ru",
"Serbian: sr-Cyrl",
"Slovak: sk",
"Slovenian: sl",
"Spanish: es",
"Swedish: sv",
"Thai: th",
"Turkish: tr",
"Ukranian: uk",
"Urdu: ur",
"Vietnamese: vi",
"Welsh: cy"
"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" } }
};
std::pair<bool, std::wstring> Translate(const std::wstring& text)
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, TranslationParam tlp)
{
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%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()
})
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->c_str(), 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\":\"(.+)\"\\,\"to"))) return { true, results[1] };
else return { false, TRANSLATION_ERROR };
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) };
}

57
extensions/blockmarkup.h Normal file
View File

@ -0,0 +1,57 @@
#pragma once
#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())
{
std::copy_n(delimiters, delimiterCount, this->delimiters.begin());
}
std::optional<std::array<std::basic_string<C>, delimiterCount>> Next()
{
std::array<std::basic_string<C>, delimiterCount> results;
Find(delimiters[0], true);
for (int i = 0; i < delimiterCount; ++i)
{
const auto delimiter = i + 1 < delimiterCount ? delimiters[i + 1] : end;
if (auto found = Find(delimiter, false)) results[i] = std::move(found.value());
else return {};
}
return results;
}
private:
std::optional<std::basic_string<C>> Find(std::basic_string_view<C> delimiter, bool discard)
{
for (int i = 0; ;)
{
int pos = buffer.find(delimiter, i);
if (pos != std::string::npos)
{
auto result = !discard ? std::optional(std::basic_string(buffer.begin(), buffer.begin() + pos)) : std::nullopt;
buffer.erase(buffer.begin(), buffer.begin() + pos + delimiter.size());
return result;
}
int oldSize = buffer.size();
buffer.resize(oldSize + blockSize);
if (!streambuf.sgetn((char*)(buffer.data() + oldSize), blockSize * sizeof(C))) return {};
i = max(0, oldSize - (int)delimiter.size());
if (discard)
{
buffer.erase(0, i);
i = 0;
}
}
}
static constexpr C endImpl[5] = { '|', 'E', 'N', 'D', '|' };
static constexpr std::basic_string_view<C> end{ endImpl, 5 };
std::basic_streambuf<char>& streambuf;
std::basic_string<C> buffer;
std::array<std::basic_string_view<C>, delimiterCount> delimiters;
};

View File

@ -0,0 +1,180 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "network.h"
#include <random>
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "DeepL Translate";
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html#developer";
extern const QStringList languagesTo
{
"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, useRateLimiter = true, rateLimitSelected = true, useCache = true, useFilter = true;
int tokenCount = 10, rateLimitTimespan = 60000, maxSentenceSize = 1000;
enum KeyType { CAT, REST };
int keyType = REST;
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
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",
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
L"POST",
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 (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;
// user_preferred_langs? what should priority be? does timestamp do anything? other translation quality options?
auto body = FormatString(R"(
{
"id": %d,
"jsonrpc": "2.0",
"method": "LMT_handle_jobs",
"params": {
"priority": -1,
"timestamp": %lld,
"lang": {
"target_lang": "%.2S",
"source_lang_user_selected": "%S"
},
"jobs": [{
"raw_en_sentence": "%s",
"raw_en_context_before": [],
"kind": "default",
"preferred_num_beams": 1,
"quality": "fast",
"raw_en_context_after": []
}]
}
}
)", 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
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"www2.deepl.com",
L"POST",
L"/jsonrpc",
body,
L"Host: www2.deepl.com\r\nAccept-Language: en-US,en;q=0.5\r\nContent-type: application/json; charset=utf-8\r\nOrigin: https://www.deepl.com\r\nTE: Trailers",
INTERNET_DEFAULT_PORT,
L"https://www.deepl.com/translator",
WINHTTP_FLAG_SECURE
})
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
View 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
View 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"{}");
}

View 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 };
}

View 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 };
}

View 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 };
}

View File

@ -1,7 +1,5 @@
#pragma once
#include "common.h"
struct InfoForExtension
{
const char* name;
@ -11,7 +9,7 @@ struct InfoForExtension
struct SentenceInfo
{
const InfoForExtension* infoArray;
int64_t operator[](std::string propertyName)
int64_t operator[](std::string_view propertyName)
{
for (auto info = infoArray; info->name; ++info) // nullptr name marks end of info array
if (propertyName == info->name) return info->value;

View File

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

View File

@ -1,24 +1,32 @@
#include "qtcommon.h"
#include "extension.h"
#include "ui_extrawindow.h"
#include "defs.h"
#include "blockmarkup.h"
#include <fstream>
#include <filesystem>
#include <process.h>
#include <QRegularExpression>
#include <QColorDialog>
#include <QFontDialog>
#include <QMenu>
#include <QPainter>
#include <QGraphicsEffect>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QScrollArea>
#include <QAbstractNativeEventFilter>
extern const char* EXTRA_WINDOW_INFO;
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;
@ -28,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)
{
@ -39,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()
@ -72,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{ CONFIG_FILE, QSettings::IniFormat, this };
Settings settings{ this };
private:
void RequestFont()
@ -85,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
{
@ -144,24 +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);
setGeometry(settings.value(WINDOW, geometry()).toRect());
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
@ -170,10 +213,15 @@ public:
action->setCheckable(true);
action->setChecked(default);
}
menu.addAction(CLICK_THROUGH, this, &ExtraWindow::ToggleClickThrough);
ui.display->installEventFilter(this);
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);
@ -186,33 +234,90 @@ public:
void AddSentence(QString sentence)
{
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()
{
SetWindowPos((HWND)winId(), topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
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)
{
@ -226,18 +331,77 @@ private:
settings.setValue(DICTIONARY, this->useDictionary = useDictionary);
}
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))
{
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, 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, flags, sentence.mid(lineBreak, i - lineBreak + 1)).right() + 1,
height
});
}
}
int i;
for (i = 0; i < textPositionMap.size(); ++i) if (textPositionMap[i].y() > mouse.y() && textPositionMap[i].x() > mouse.x()) break;
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));
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::MouseButtonRelease && ui.display->hasSelectedText())
{
dictionaryWindow.ui.display->setFixedWidth(ui.display->width());
dictionaryWindow.setTerm(ui.display->text().mid(ui.display->selectionStart()));
dictionaryWindow.move({ x(), y() - dictionaryWindow.height() });
}
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();
@ -246,27 +410,49 @@ 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;
bool sizeLock, posLock, centeredText, autoResize, showOriginal, showOriginalAfterTranslation, useDictionary, clickThrough;
QPoint oldPos;
class
{
public:
bool CompareExchange(QLabel* display)
{
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;
}
private:
QString text;
QFont font;
int width;
Qt::Alignment alignment;
} cachedDisplayInfo;
std::vector<QPoint> textPositionMap;
std::vector<QString> sentenceHistory;
int historyIndex = 0;
class DictionaryWindow : public PrettyWindow
{
public:
DictionaryWindow() :
PrettyWindow("Dictionary Window")
DictionaryWindow() : PrettyWindow("Dictionary Window")
{
ui.display->setSizePolicy({ QSizePolicy::Fixed, QSizePolicy::Minimum });
}
@ -281,53 +467,72 @@ private:
catch (std::filesystem::filesystem_error) { return; }
dictionary.clear();
owningStorage.clear();
charStorage.clear();
auto StoreCopy = [&](const std::string& string)
auto StoreCopy = [&](std::string_view string)
{
return &*owningStorage.insert(owningStorage.end(), string.c_str(), string.c_str() + string.size() + 1);
auto location = &*charStorage.insert(charStorage.end(), string.begin(), string.end());
charStorage.push_back(0);
return location;
};
std::string savedDictionary(std::istreambuf_iterator(std::ifstream(DICTIONARY_SAVE_FILE)), {});
owningStorage.reserve(savedDictionary.size());
for (size_t end = 0; ;)
charStorage.reserve(std::filesystem::file_size(DICTIONARY_SAVE_FILE));
std::ifstream stream(DICTIONARY_SAVE_FILE);
BlockMarkupIterator savedDictionary(stream, Array<std::string_view>{ "|TERM|", "|DEFINITION|" });
while (auto read = savedDictionary.Next())
{
size_t term = savedDictionary.find("|TERM|", end);
size_t definition = savedDictionary.find("|DEFINITION|", term);
if ((end = savedDictionary.find("|END|", definition)) == std::string::npos) break;
auto storedDefinition = StoreCopy(savedDictionary.substr(definition + 12, end - definition - 12));
for (size_t next; (next = savedDictionary.find("|TERM|", term + 1)) != std::string::npos && next < definition; term = next)
dictionary.push_back({ StoreCopy(savedDictionary.substr(term + 6, next - term - 6)), storedDefinition });
dictionary.push_back({ StoreCopy(savedDictionary.substr(term + 6, definition - term - 6)), storedDefinition });
const auto& [terms, definition] = read.value();
auto storedDefinition = StoreCopy(definition);
std::string_view termsView = terms;
size_t start = 0, end = termsView.find("|TERM|");
while (end != std::string::npos)
{
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start, end - start)), storedDefinition });
start = end + 6;
end = termsView.find("|TERM|", start);
}
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start)), storedDefinition });
}
auto oldData = owningStorage.data();
owningStorage.shrink_to_fit();
dictionary.shrink_to_fit();
for (auto& [term, definition] : dictionary)
std::stable_sort(dictionary.begin(), dictionary.end());
inflections.clear();
stream.seekg(0);
BlockMarkupIterator savedInflections(stream, Array<std::string_view>{ "|ROOT|", "|INFLECTS TO|", "|NAME|" });
while (auto read = savedInflections.Next())
{
term += owningStorage.data() - oldData;
definition += owningStorage.data() - oldData;
const auto& [root, inflectsTo, name] = read.value();
if (!inflections.emplace_back(Inflection{
S(root),
QRegularExpression(QRegularExpression::anchoredPattern(S(inflectsTo)), QRegularExpression::UseUnicodePropertiesOption),
S(name)
}).inflectsTo.isValid()) TEXTRACTOR_MESSAGE(L"Invalid regex: %s", StringToWideString(inflectsTo));
}
std::sort(dictionary.begin(), dictionary.end());
}
void setTerm(QString term)
void SetTerm(QString term)
{
this->term = term;
UpdateDictionary();
definitions.clear();
definitionIndex = 0;
std::unordered_set<std::string_view> definitionSet;
for (QByteArray utf8term = term.left(200).toUtf8(); !utf8term.isEmpty(); utf8term.chop(1))
for (auto [it, end] = std::equal_range(dictionary.begin(), dictionary.end(), DictionaryEntry{ utf8term }); it != end; ++it)
if (definitionSet.emplace(it->definition).second)
definitions.push_back(QStringLiteral("<h3>%1 (%3/%4)</h3>%2").arg(utf8term, it->definition));
std::unordered_set<const char*> foundDefinitions;
for (term = term.left(100); !term.isEmpty(); term.chop(1))
for (const auto& [rootTerm, definition, inflections] : LookupDefinitions(term, foundDefinitions))
definitions.push_back(
QStringLiteral("<h3>%1 (%5/%6)</h3><small>%2%3</small>%4").arg(
term.split("<<")[0].toHtmlEscaped(),
rootTerm.split("<<")[0].toHtmlEscaped(),
inflections.join(""),
definition
)
);
for (int i = 0; i < definitions.size(); ++i) definitions[i] = definitions[i].arg(i + 1).arg(definitions.size());
ShowDefinition();
}
void ShowDefinition()
{
if (definitions.empty()) return;
if (definitions.empty()) return hide();
ui.display->setText(definitions[definitionIndex]);
adjustSize();
resize(width(), 1);
@ -341,8 +546,31 @@ private:
bool operator<(DictionaryEntry other) const { return strcmp(term, other.term) < 0; }
};
std::vector<DictionaryEntry> dictionary;
QString term;
private:
struct LookupResult
{
QString term;
QString definition;
QStringList inflectionsUsed;
};
std::vector<LookupResult> LookupDefinitions(QString term, std::unordered_set<const char*>& foundDefinitions, QStringList inflectionsUsed = {})
{
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 });
for (const auto& inflection : inflections) if (auto match = inflection.inflectsTo.match(term); match.hasMatch())
{
QStringList currentInflectionsUsed = inflectionsUsed;
currentInflectionsUsed.push_front(inflection.name);
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;
}
void wheelEvent(QWheelEvent* event) override
{
int scroll = event->angleDelta().y();
@ -353,8 +581,16 @@ private:
move(x(), y() + oldHeight - height());
}
struct Inflection
{
QString root;
QRegularExpression inflectsTo;
QString name;
};
std::vector<Inflection> inflections;
std::filesystem::file_time_type dictionaryFileLastWrite;
std::vector<char> owningStorage;
std::vector<char> charStorage;
std::vector<QString> definitions;
int definitionIndex;
} dictionaryWindow;

View File

@ -6,10 +6,13 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<width>800</width>
<height>300</height>
</rect>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="display">
@ -27,6 +30,9 @@
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="alignment">
<set>Qt::AlignTop</set>
</property>

View File

@ -1,127 +1,267 @@
#include "extension.h"
#include "qtcommon.h"
#include "translatewrapper.h"
#include "network.h"
#include "util.h"
#include <ctime>
#include <QStringList>
extern const wchar_t* TRANSLATION_ERROR;
extern Synchronized<std::wstring> translateTo;
const char* TRANSLATION_PROVIDER = "Google";
QStringList languages
const char* TRANSLATION_PROVIDER = "Google Translate";
const char* GET_API_KEY_FROM = "https://console.cloud.google.com/marketplace/product/google/translate.googleapis.com";
extern const QStringList languagesTo
{
"English: en",
"Afrikaans: af",
"Arabic: ar",
"Albanian: sq",
"Belarusian: be",
"Bengali: bn",
"Bosnian: bs",
"Bulgarian: bg",
"Catalan: ca",
"Chinese(Simplified): zh-CH",
"Chinese(Traditional): zh-TW",
"Croatian: hr",
"Czech: cs",
"Danish: da",
"Dutch: nl",
"Esperanto: eo",
"Estonian: et",
"Filipino: tl",
"Finnish: fi",
"French: fr",
"Galician: gl",
"German: de",
"Greek: el",
"Hebrew: iw",
"Hindi: hi",
"Hungarian: hu",
"Icelandic: is",
"Indonesian: id",
"Irish: ga",
"Italian: it",
"Japanese: ja",
"Klingon: tlh",
"Korean: ko",
"Latin: la",
"Latvian: lv",
"Lithuanian: lt",
"Macedonian: mk",
"Malay: ms",
"Maltese: mt",
"Norwegian: no",
"Persian: fa",
"Polish: pl",
"Portuguese: pt",
"Romanian: ro",
"Russian: ru",
"Serbian: sr",
"Slovak: sk",
"Slovenian: sl",
"Somali: so",
"Spanish: es",
"Swahili: sw",
"Swedish: sv",
"Thai: th",
"Turkish: tr",
"Ukranian: uk",
"Urdu: ur",
"Vietnamese: vi",
"Welsh: cy",
"Yiddish: yi",
"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" } }
};
unsigned TKK = 0;
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
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->c_str(), text);
// Artikash 8/19/2018: 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;
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%s", codes.at(tlp.translateTo), tlp.authKey, translateFromComponent).c_str(),
FormatString(R"({"q":["%s"]})", JSON::Escape(WideStringToString(text)))
})
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) };
}
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->c_str(), a, a ^ b, escapedText);
}
bool IsHash(const std::wstring& result)
{
return result.size() == 32 && std::all_of(result.begin(), result.end(), [](char ch) { return (ch >= L'0' && ch <= L'9') || (ch >= L'a' && ch <= L'z'); });
}
std::pair<bool, std::wstring> Translate(const std::wstring& text)
{
if (!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)", TRANSLATION_ERROR, _InterlockedExchange(&TKK, 0)) };
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) };
}

View File

@ -1,11 +1,9 @@
#include "qtcommon.h"
#include "extension.h"
#include "util.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";
@ -42,10 +40,10 @@ bool logErrors = true;
Synchronized<std::string> script;
std::atomic<int> revCount = 0;
class Window : public QMainWindow
class Window : public QDialog, Localizer
{
public:
Window()
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
@ -54,9 +52,10 @@ public:
layout.addWidget(&loadButton);
resize(800, 600);
setCentralWidget(&centralWidget);
setWindowTitle("Lua");
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
LoadScript();
}
~Window()
@ -77,23 +76,22 @@ private:
QTextFile(LUA_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
}
QWidget centralWidget{ this };
QHBoxLayout layout{ &centralWidget };
QPlainTextEdit scriptEditor{ QTextFile(LUA_SAVE_FILE, QIODevice::ReadOnly).readAll(), &centralWidget };
QPushButton loadButton{ LOAD_LUA_SCRIPT, &centralWidget };
QHBoxLayout layout{ this };
QPlainTextEdit scriptEditor{ QTextFile(LUA_SAVE_FILE, QIODevice::ReadOnly).readAll(), 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)
{
revCount = ::revCount;
luaL_dostring(L, "ProcessSentence = nil");
if (luaL_dostring(L, script->c_str()) != LUA_OK)
if (luaL_dostring(L, script.Copy().c_str()) != LUA_OK)
{
sentence += L"\n" + FormatString(LUA_ERROR, StringToWideString(lua_tolstring(L, 1, nullptr)));
lua_settop(L, 0);

View File

@ -1,28 +1,32 @@
#include "network.h"
#include "util.h"
HttpRequest::HttpRequest(
const wchar_t* agentName,
const wchar_t* serverName,
const wchar_t* action,
const wchar_t* objectName,
std::string body,
const wchar_t* headers,
DWORD port,
const wchar_t* referrer,
DWORD requestFlags,
const wchar_t* httpVersion,
const wchar_t* referrer,
const wchar_t** acceptTypes,
const wchar_t* headers,
void* body,
DWORD bodyLength
const wchar_t** acceptTypes
)
{
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, bodyLength, bodyLength, NULL))
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);
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, this->headers.data(), &size, WINHTTP_NO_HEADER_INDEX);
std::string data;
DWORD availableSize, downloadedSize;
do
@ -51,17 +55,11 @@ std::wstring Escape(const std::wstring& text)
return escaped;
}
void Unescape(std::wstring& text)
std::string Escape(const std::string& text)
{
for (int i = 0; i < text.size(); ++i)
{
if (text[i] == L'\\')
{
text[i] = 0x200b;
if (text[i + 1] == L'r') text[i + 1] = 0x200b; // 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;
}
}
std::string escaped;
for (unsigned char ch : text) escaped += FormatString("%%%02X", (int)ch);
return escaped;
}
TEST(assert(JSON::Parse<wchar_t>(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])")));

View File

@ -1,7 +1,7 @@
#pragma once
#include "common.h"
#include <winhttp.h>
#include <variant>
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
@ -12,21 +12,222 @@ struct HttpRequest
const wchar_t* serverName,
const wchar_t* action,
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,
const wchar_t* referrer = NULL,
const wchar_t** acceptTypes = NULL,
const wchar_t* headers = NULL,
void* body = NULL,
DWORD bodyLength = 0
const wchar_t** acceptTypes = NULL
);
operator bool() { return errorCode == ERROR_SUCCESS; }
std::wstring response;
std::wstring headers;
InternetHandle connection = NULL;
InternetHandle request = NULL;
DWORD errorCode = ERROR_SUCCESS;
};
std::wstring Escape(const std::wstring& text);
void Unescape(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
{
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);
}
}

View File

@ -1,43 +1,72 @@
#include "qtcommon.h"
#include "extension.h"
#include "ui_regexfilter.h"
#include "module.h"
#include "blockmarkup.h"
#include <fstream>
extern const char* REGEX_FILTER;
extern const char* INVALID_REGEX;
extern const char* CURRENT_FILTER;
std::wregex regex;
std::shared_mutex m;
const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt";
class Window : public QMainWindow
std::optional<std::wregex> regex;
std::wstring replace = L"$1";
concurrency::reader_writer_lock m;
DWORD (*GetSelectedProcessId)() = [] { return 0UL; };
class Window : public QDialog, Localizer
{
public:
Window()
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
ui.setupUi(this);
connect(ui.input, &QLineEdit::textEdited, this, &Window::setRegex);
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);
}
private:
void setRegex(QString regex)
void SetRegex(QString regex)
{
std::lock_guard l(m);
try { ::regex = S(regex); }
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;
ui.output->setText(QString(CURRENT_FILTER).arg(regex));
}
private:
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.regexEdit->text())
);
std::ofstream(REGEX_SAVE_FILE, std::ios::binary | std::ios::app).write((const char*)formatted.c_str(), formatted.size() * sizeof(wchar_t));
}
Ui::FilterWindow ui;
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
static auto _ = GetSelectedProcessId = (DWORD(*)())sentenceInfo["get selected process id"];
if (sentenceInfo["text number"] == 0) return false;
std::shared_lock l(m);
sentence = std::regex_replace(sentence, regex, L"");
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, std::bind(&Window::SetRegex, &window, S(regexes.back())), Qt::BlockingQueuedConnection);
}
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
if (regex) sentence = std::regex_replace(sentence, regex.value(), replace);
return true;
}

View File

@ -1,51 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FilterWindow</class>
<widget class="QMainWindow" name="FilterWindow">
<widget class="QDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>76</height>
<width>500</width>
<height>80</height>
</rect>
</property>
<widget class="QWidget">
<layout class="QVBoxLayout">
<item>
<widget class="QLineEdit" name="input"/>
</item>
<item>
<widget class="QLabel" name="output">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel">
<property name="text">
<string>&lt;a href=&quot;https://regexr.com&quot;&gt;regexr.com&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
<layout class="QVBoxLayout">
<item>
<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">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel">
<property name="text">
<string>&lt;a href=&quot;https://regexr.com&quot;&gt;regexr.com&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>

View 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;
}

View File

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

View File

@ -2,34 +2,34 @@
std::vector<int> GenerateSuffixArray(const std::wstring& text)
{
std::vector<int> identity(text.size());
for (int i = 0; i < text.size(); ++i) identity[i] = i;
std::vector<int> suffixArray = identity;
std::vector<int> suffixArray(text.size());
for (int i = 0; i < text.size(); ++i) suffixArray[i] = i;
// The below code is a more efficient way of doing this:
// std::sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return wcscmp(text.c_str() + a, text.c_str() + b) > 0; });
std::stable_sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return text[a] > text[b]; });
std::vector<int> classes(text.begin(), text.end());
std::vector<int> eqClasses(text.begin(), text.end());
std::vector<int> count(text.size());
for (int length = 1; length < text.size(); length *= 2)
{
// Determine equivalence class up to length, by checking length / 2 equivalence of suffixes and their following length / 2 suffixes
std::vector<int> oldClasses = classes;
classes[suffixArray[0]] = 0;
std::vector<int> prevEqClasses = eqClasses;
eqClasses[suffixArray[0]] = 0;
for (int i = 1; i < text.size(); ++i)
{
int currentSuffix = suffixArray[i];
int lastSuffix = suffixArray[i - 1];
if (currentSuffix + length < text.size() && oldClasses[currentSuffix] == oldClasses[lastSuffix] &&
oldClasses[currentSuffix + length / 2] == oldClasses.at(lastSuffix + length / 2)) // not completely certain that this will stay in range
classes[currentSuffix] = classes[lastSuffix];
else classes[currentSuffix] = i;
int currentSuffix = suffixArray[i], lastSuffix = suffixArray[i - 1];
if (currentSuffix + length < text.size() && prevEqClasses[currentSuffix] == prevEqClasses[lastSuffix] &&
prevEqClasses[currentSuffix + length / 2] == prevEqClasses[lastSuffix + length / 2]
)
eqClasses[currentSuffix] = eqClasses[lastSuffix];
else eqClasses[currentSuffix] = i;
}
// Sort within equivalence class based on order of following suffix after length (orders up to length * 2)
std::vector<int> count = identity;
for (int i = 0; i < text.size(); ++i) count[i] = i;
for (auto suffix : std::vector(suffixArray))
{
int precedingSuffix = suffix - length;
if (precedingSuffix >= 0) suffixArray[count[classes[precedingSuffix]]++] = precedingSuffix;
if (precedingSuffix >= 0) suffixArray[count[eqClasses[precedingSuffix]]++] = precedingSuffix;
}
}
for (int i = 0; i + 1 < text.size(); ++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]));

View File

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

View File

@ -1,7 +1,8 @@
#include "extension.h"
#include "blockmarkup.h"
#include <cwctype>
#include <fstream>
#include <filesystem>
#include <sstream>
#include <process.h>
extern const wchar_t* REPLACER_INSTRUCTIONS;
@ -9,19 +10,21 @@ 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
{
public:
Trie(std::unordered_map<std::wstring, std::wstring> replacements)
Trie(const std::istream& replacementScript)
{
for (const auto& [original, replacement] : replacements)
BlockMarkupIterator replacementScriptParser(replacementScript, Array<std::wstring_view>{ L"|ORIG|", L"|BECOMES|" });
while (auto read = replacementScriptParser.Next())
{
const auto& [original, replacement] = read.value();
Node* current = &root;
for (auto ch : original) if (!Ignore(ch)) current = Next(current, ch);
if (current != &root)
current->value = owningStorage.insert(owningStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - owningStorage.begin();
current->value = charStorage.insert(charStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - charStorage.begin();
}
}
@ -38,10 +41,10 @@ public:
{
if (current->value >= 0)
{
replacement = owningStorage.data() + current->value;
replacement = charStorage.data() + current->value;
originalLength = j - i;
}
if (!Ignore(sentence[j])) current = Next(current, sentence[j]);
if (!Ignore(sentence[j])) current = Next(current, sentence[j]) ? Next(current, sentence[j]) : Next(current, L'^');
}
result += replacement;
@ -58,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>
@ -76,30 +79,16 @@ private:
ptrdiff_t value = -1;
} root;
std::vector<wchar_t> owningStorage;
} trie = { {} };
std::unordered_map<std::wstring, std::wstring> Parse(std::wstring_view replacementScript)
{
std::unordered_map<std::wstring, std::wstring> replacements;
for (size_t end = 0; ;)
{
size_t original = replacementScript.find(L"|ORIG|", end);
size_t becomes = replacementScript.find(L"|BECOMES|", original);
if ((end = replacementScript.find(L"|END|", becomes)) == std::wstring::npos) break;
replacements[std::wstring(replacementScript.substr(original + 6, becomes - original - 6))] = replacementScript.substr(becomes + 9, end - becomes - 9);
}
return replacements;
}
std::vector<wchar_t> charStorage;
} trie = { std::istringstream("") };
void UpdateReplacements()
{
try
{
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
std::vector<BYTE> file(std::istreambuf_iterator(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary)), {});
std::scoped_lock l(m);
trie = Trie(Parse({ (wchar_t*)file.data(), file.size() / sizeof(wchar_t) }));
std::scoped_lock lock(m);
trie = Trie(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary));
}
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
}
@ -114,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;
@ -131,19 +121,19 @@ 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;
}
TEST(
{
auto replacements = Parse(LR"(
std::wstring replacementScript = LR"(
|ORIG||BECOMES|goodbye |END|Ignore this text
And this text   
|ORIG||BECOMES|idiot|END|
|ORIG| |BECOMES| hello|END||ORIG|delete this|BECOMES||END|)");
assert(replacements.size() == 4);
|ORIG| |BECOMES| hello|END||ORIG|delet^this|BECOMES||END|)";
Trie replacements(std::istringstream(std::string{ (const char*)replacementScript.c_str(), replacementScript.size() * sizeof(wchar_t) }));
std::wstring original = LR"(Don't replace this 
delete this)";
std::wstring replaced = Trie(std::move(replacements)).Replace(original);

54
extensions/styler.cpp Normal file
View 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;
}

View File

@ -1,27 +1,31 @@
#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 QMainWindow
class Window : public QDialog, Localizer
{
public:
Window()
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
connect(&linkButton, &QPushButton::clicked, this, &Window::Link);
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);
layout.addWidget(&linkList);
layout.addWidget(&linkButton);
setCentralWidget(&centralWidget);
setWindowTitle(THREAD_LINKER);
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
}
@ -30,41 +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();
}
QWidget centralWidget{ this };
QHBoxLayout layout{ &centralWidget };
QListWidget linkList{ &centralWidget };
QPushButton linkButton{ LINK, &centralWidget };
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(*)(void*, int64_t, const wchar_t*))sentenceInfo["void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)"])
((void*)sentenceInfo["this"], 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;
}

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

View File

@ -1,75 +1,154 @@
#include "qtcommon.h"
#include "extension.h"
#include "defs.h"
#include "network.h"
#include <map>
#include <QTimer>
#include "translatewrapper.h"
#include "blockmarkup.h"
#include <concurrent_priority_queue.h>
#include <fstream>
#include <QComboBox>
extern const char* SELECT_LANGUAGE;
extern const char* SELECT_LANGUAGE_MESSAGE;
extern const char* LANGUAGE_SAVED;
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* 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 QStringList languages;
std::pair<bool, std::wstring> Translate(const std::wstring& text);
extern const char* GET_API_KEY_FROM;
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);
const char* LANGUAGE = u8"Language";
const QString CACHE_FILE = QString("%1 Cache.txt").arg(TRANSLATION_PROVIDER);
QFormLayout* display;
Settings settings;
Synchronized<std::wstring> translateTo = L"en";
QSettings settings(CONFIG_FILE, QSettings::IniFormat);
int savedSize;
Synchronized<std::map<std::wstring, std::wstring>> translationCache;
void SaveCache()
namespace
{
QTextFile file(CACHE_FILE, QIODevice::WriteOnly | QIODevice::Truncate);
auto translationCache = ::translationCache.Acquire();
for (const auto& [original, translation] : translationCache.contents)
file.write(S(FormatString(L"%s|T|\n%s|T|\n", original, translation)).toUtf8());
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));
}
}
}
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
class Window : public QDialog, Localizer
{
switch (ul_reason_for_call)
public:
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
case DLL_PROCESS_ATTACH:
{
settings.beginGroup(TRANSLATION_PROVIDER);
if (settings.contains(LANGUAGE)) translateTo->assign(S(settings.value(LANGUAGE).toString()));
else QTimer::singleShot(0, []
{
QString language = QInputDialog::getItem(
nullptr,
SELECT_LANGUAGE,
QString(SELECT_LANGUAGE_MESSAGE).arg(TRANSLATION_PROVIDER),
languages,
0,
false,
nullptr,
Qt::WindowCloseButtonHint
);
translateTo->assign(S(language.split(": ")[1]));
settings.setValue(LANGUAGE, S(translateTo->c_str()));
QMessageBox::information(nullptr, SELECT_LANGUAGE, QString(LANGUAGE_SAVED).arg(CONFIG_FILE));
});
display = new QFormLayout(this);
QStringList savedCache = QString(QTextFile(CACHE_FILE, QIODevice::ReadOnly).readAll()).split("|T|\n", QString::SkipEmptyParts);
for (int i = 0; i < savedCache.size() - 1; i += 2)
translationCache->insert({ S(savedCache[i]), S(savedCache[i + 1]) });
savedSize = translationCache->size();
settings.beginGroup(TRANSLATION_PROVIDER);
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 },
{ useRateLimiter, RATE_LIMIT_ALL_THREADS },
{ rateLimitSelected, RATE_LIMIT_SELECTED_THREAD },
{ useCache, USE_TRANS_CACHE },
{ useFilter, FILTER_GARBAGE }
})
{
value = settings.value(label, value).toBool();
auto checkBox = new QCheckBox(this);
checkBox->setChecked(value);
display->addRow(label, checkBox);
connect(checkBox, &QCheckBox::clicked, [label, &value](bool checked) { settings.setValue(label, value = checked); });
}
for (auto [value, label] : Array<int&, const char*>{
{ tokenCount, MAX_TRANSLATIONS_IN_TIMESPAN },
{ rateLimitTimespan, TIMESPAN },
{ maxSentenceSize, MAX_SENTENCE_SIZE },
})
{
value = settings.value(label, value).toInt();
auto spinBox = new QSpinBox(this);
spinBox->setRange(0, INT_MAX);
spinBox->setValue(value);
display->addRow(label, spinBox);
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [label, &value](int newValue) { settings.setValue(label, value = newValue); });
}
if (GET_API_KEY_FROM)
{
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, keyEdit);
}
setWindowTitle(TRANSLATION_PROVIDER);
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
}
break;
case DLL_PROCESS_DETACH:
~Window()
{
SaveCache();
}
break;
private:
void SaveTranslateTo(QString language)
{
SaveCache();
settings.setValue(TRANSLATE_TO, S(tlp->translateTo = S(language)));
LoadCache();
}
return TRUE;
}
void SaveTranslateFrom(QString language)
{
settings.setValue(TRANSLATE_FROM, S(tlp->translateFrom = S(language)));
}
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
@ -80,34 +159,54 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
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(), [this](DWORD token) { return GetTickCount() - token > delay; }), 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:
const int tokenCount = 30, delay = 60 * 1000;
Synchronized<std::vector<DWORD>> tokens;
concurrency::concurrent_priority_queue<DWORD64, std::greater<DWORD64>> tokens;
} rateLimiter;
bool cache = false;
std::wstring translation;
if (translationCache->count(sentence) != 0) translation = translationCache->at(sentence);
else if (!(rateLimiter.Request() || sentenceInfo["current select"])) translation = TOO_MANY_TRANS_REQUESTS;
else std::tie(cache, translation) = Translate(sentence);
if (cache && sentenceInfo["current select"])
if (useFilter)
{
translationCache->insert({ sentence, translation });
if (translationCache->size() > savedSize + 50) SaveCache();
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;
}
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
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->operator[](sentence) = translation;
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;
}
extern const std::unordered_map<std::wstring, std::wstring> codes;
TEST(
assert(Translate(L"こんにちは").second.find(L"ello") != std::wstring::npos)
{
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"?"));
}
);

View File

@ -0,0 +1,6 @@
#pragma once
struct TranslationParam
{
std::wstring translateTo, translateFrom, authKey;
};

View File

@ -1,17 +0,0 @@
#pragma once
#include "common.h"
inline std::wstring StringToWideString(const std::string& text)
{
std::vector<wchar_t> buffer(text.size() + 1);
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size());
return buffer.data();
}
inline std::string WideStringToString(const std::wstring& text)
{
std::vector<char> buffer((text.size() + 1) * 4);
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size(), nullptr, nullptr);
return buffer.data();
}

4
host/CLI/CMakeLists.txt Normal file
View 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)

View File

@ -1,5 +1,5 @@
#include "host.h"
#include "util.h"
#include "../host.h"
#include "../hookcode.h"
#include <io.h>
#include <fcntl.h>
#include <iostream>
@ -12,7 +12,16 @@ int main()
fflush(stdout);
Host::Start([](auto) {}, [](auto) {}, [](auto&) {}, [](auto&) {}, [](TextThread& thread, std::wstring& output)
{
wprintf_s(L"[%I64X:%I32X:%I64X:%I64X:%I64X:%s:%s] %s\n", thread.handle, thread.tp.processId, thread.tp.addr, thread.tp.ctx, thread.tp.ctx2, thread.name.c_str(), Util::GenerateCode(thread.hp, thread.tp.processId).c_str(), output.c_str());
wprintf_s(L"[%I64X:%I32X:%I64X:%I64X:%I64X:%s:%s] %s\n",
thread.handle,
thread.tp.processId,
thread.tp.addr,
thread.tp.ctx,
thread.tp.ctx2,
thread.name.c_str(),
HookCode::Generate(thread.hp, thread.tp.processId).c_str(),
output.c_str()
);
fflush(stdout);
return false;
});
@ -24,7 +33,7 @@ int main()
if (swscanf(input, L"%500s -P%d", command, &processId) != 2) ExitProcess(0);
if (_wcsicmp(command, L"attach") == 0) Host::InjectProcess(processId);
else if (_wcsicmp(command, L"detach") == 0) Host::DetachProcess(processId);
else if (auto hp = Util::ParseCode(command)) Host::InsertHook(processId, hp.value());
else if (auto hp = HookCode::Parse(command)) Host::InsertHook(processId, hp.value());
else ExitProcess(0);
}
ExitProcess(0);

8
host/CMakeLists.txt Normal file
View File

@ -0,0 +1,8 @@
add_library(host
host.cpp
textthread.cpp
hookcode.cpp
)
target_precompile_headers(host REUSE_FROM pch)
add_subdirectory(CLI)

View File

@ -1,5 +1,5 @@
#include "util.h"
#include <Psapi.h>
#include "hookcode.h"
#include "module.h"
namespace
{
@ -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)
{
@ -250,7 +257,7 @@ namespace
if (processId && !(hp.type & MODULE_OFFSET))
if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId))
if (MEMORY_BASIC_INFORMATION info = {}; VirtualQueryEx(process, (LPCVOID)hp.address, &info, sizeof(info)))
if (auto moduleName = Util::GetModuleFilename(processId, (HMODULE)info.AllocationBase))
if (auto moduleName = GetModuleFilename(processId, (HMODULE)info.AllocationBase))
{
hp.type |= MODULE_OFFSET;
hp.address -= (uint64_t)info.AllocationBase;
@ -259,80 +266,39 @@ namespace
HCode += L'@' + HexString(hp.address);
if (hp.type & MODULE_OFFSET) HCode += L':' + std::wstring(hp.module);
if (hp.type & FUNCTION_OFFSET) HCode += L':' + std::wstring(hp.function, hp.function + MAX_MODULE_SIZE);
if (hp.type & FUNCTION_OFFSET) HCode += L':' + StringToWideString(hp.function);
return HCode;
}
}
namespace Util
namespace HookCode
{
std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module)
std::optional<HookParam> Parse(std::wstring code)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId))
if (GetModuleFileNameExW(process, module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
std::optional<std::wstring> GetModuleFilename(HMODULE module)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (GetModuleFileNameW(module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses()
{
std::vector<DWORD> processIds(10000);
DWORD spaceUsed = 0;
EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed);
std::vector<std::pair<DWORD, std::optional<std::wstring>>> processes;
for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], Util::GetModuleFilename(processIds[i]) });
return processes;
}
std::optional<std::wstring> GetClipboardText()
{
if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) return {};
if (!OpenClipboard(NULL)) return {};
std::optional<std::wstring> text;
if (AutoHandle<Functor<GlobalUnlock>> clipboard = GetClipboardData(CF_UNICODETEXT)) text = (wchar_t*)GlobalLock(clipboard);
CloseClipboard();
return text;
}
std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding)
{
std::vector<wchar_t> buffer(text.size() + 1);
if (int length = MultiByteToWideChar(encoding, 0, text.c_str(), text.size() + 1, buffer.data(), buffer.size()))
return std::wstring(buffer.data(), length - 1);
return {};
}
std::optional<HookParam> ParseCode(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 {};
}
std::wstring GenerateCode(HookParam hp, DWORD processId)
std::wstring Generate(HookParam hp, DWORD processId)
{
return hp.type & DIRECT_READ ? GenerateRCode(hp) : GenerateHCode(hp, processId);
}
TEST(
assert(StringToWideString(u8"こんにちは").value() == L"こんにちは"),
assert(StringToWideString(u8"こんにちは") == L"こんにちは"),
assert(HexString(-12) == L"-C"),
assert(HexString(12) == L"C"),
assert(ParseCode(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA")),
assert(ParseCode(L"HB4@0")),
assert(ParseCode(L"/RS65001#@44")),
assert(!ParseCode(L"HQ@4")),
assert(!ParseCode(L"/RW@44")),
assert(!ParseCode(L"/HWG@33"))
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"/RW@44")),
assert(!Parse(L"/HWG@33"))
);
}

9
host/hookcode.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include "types.h"
namespace HookCode
{
std::optional<HookParam> Parse(std::wstring code);
std::wstring Generate(HookParam hp, DWORD processId = 0);
}

View File

@ -1,8 +1,8 @@
#include "host.h"
#include "defs.h"
#include "util.h"
#include "module.h"
#include "hookcode.h"
#include "../texthook/texthook.h"
#include <filesystem>
extern const wchar_t* ALREADY_INJECTED;
extern const wchar_t* NEED_32_BIT;
@ -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 {};
}
@ -49,7 +48,7 @@ namespace
Host::HookEventHandler OnHookFound = [](HookParam hp, std::wstring text)
{
Host::AddConsoleOutput(Util::GenerateCode(hp) + L": " + text);
Host::AddConsoleOutput(HookCode::Generate(hp) + L": " + text);
};
private:
@ -69,10 +68,7 @@ namespace
void RemoveThreads(std::function<bool(ThreadParam)> removeIf)
{
std::vector<TextThread*> threadsToRemove;
{
auto textThreadsByParams = ::textThreadsByParams.Acquire();
std::for_each(textThreadsByParams->begin(), textThreadsByParams->end(), [&](auto& it) { if (removeIf(it.first)) threadsToRemove.push_back(&it.second); });
}
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (removeIf(tp)) threadsToRemove.push_back(&thread);
for (auto thread : threadsToRemove)
{
OnDestroy(*thread);
@ -110,9 +106,9 @@ namespace
std::wstring wide = info.text;
if (wide.size() > STRING) OnHookFound(info.hp, std::move(info.text));
info.hp.type &= ~USING_UNICODE;
if (auto converted = Util::StringToWideString((char*)info.text, info.hp.codepage))
if (auto converted = StringToWideString((char*)info.text, info.hp.codepage))
if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value()));
if (auto converted = Util::StringToWideString((char*)info.text, info.hp.codepage = CP_UTF8))
if (auto converted = StringToWideString((char*)info.text, info.hp.codepage = CP_UTF8))
if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value()));
}
break;
@ -125,21 +121,21 @@ namespace
case HOST_NOTIFICATION_TEXT:
{
auto info = *(ConsoleOutputNotif*)buffer;
Host::AddConsoleOutput(Util::StringToWideString(info.message).value());
Host::AddConsoleOutput(StringToWideString(info.message));
}
break;
default:
{
auto tp = *(ThreadParam*)buffer;
auto textThreadsByParams = ::textThreadsByParams.Acquire();
auto textThread = textThreadsByParams->find(tp);
if (textThread == textThreadsByParams->end())
auto thread = textThreadsByParams->find(tp);
if (thread == textThreadsByParams->end())
{
try { textThread = textThreadsByParams->try_emplace(tp, tp, processRecordsByIds->at(tp.processId).GetHook(tp.addr).hp).first; }
try { thread = textThreadsByParams->try_emplace(tp, tp, processRecordsByIds->at(tp.processId).GetHook(tp.addr).hp).first; }
catch (std::out_of_range) { continue; } // probably garbage data in pipe, try again
OnCreate(textThread->second);
OnCreate(thread->second);
}
textThread->second.Push(buffer + sizeof(tp), bytesRead - sizeof(tp));
thread->second.Push(buffer + sizeof(tp), bytesRead - sizeof(tp));
}
break;
}
@ -182,8 +178,12 @@ namespace Host
for (int retry = 0; !clipboardText && retry < 3; ++retry) // retry loop in case something else is using the clipboard
{
Sleep(10);
if (clipboardText = Util::GetClipboardText()) GetThread(clipboard).AddSentence(std::move(clipboardText.value()));
if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) continue;
if (!OpenClipboard(NULL)) continue;
if (AutoHandle<Functor<GlobalUnlock>> clipboard = GetClipboardData(CF_UNICODETEXT)) clipboardText = (wchar_t*)GlobalLock(clipboard);
CloseClipboard();
}
if (clipboardText) GetThread(clipboard).AddSentence(std::move(clipboardText.value()));
}
throw;
}).detach();
@ -205,7 +205,7 @@ namespace Host
IsWow64Process(process, &invalidProcess);
if (invalidProcess) return AddConsoleOutput(NEED_32_BIT);
#endif
static std::wstring location = std::filesystem::current_path().wstring() + L"\\" + ITH_DLL;
static std::wstring location = std::filesystem::path(GetModuleFilename().value()).replace_filename(ITH_DLL);
if (LPVOID remoteData = VirtualAllocEx(process, nullptr, (location.size() + 1) * sizeof(wchar_t), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))
{
WriteProcessMemory(process, remoteData, location.c_str(), (location.size() + 1) * sizeof(wchar_t), nullptr);
@ -248,9 +248,8 @@ namespace Host
TextThread* GetThread(int64_t handle)
{
auto textThreadsByParams = ::textThreadsByParams.Acquire();
auto thread = std::find_if(textThreadsByParams->begin(), textThreadsByParams->end(), [&](const auto& thread) { return thread.second.handle == handle; });
return thread != textThreadsByParams->end() ? &thread->second : nullptr;
for (auto& [tp, thread] : textThreadsByParams.Acquire().contents) if (thread.handle == handle) return &thread;
return nullptr;
}
void AddConsoleOutput(std::wstring text)

View File

@ -1,11 +1,10 @@
#pragma once
#include "common.h"
#include "textthread.h"
namespace Host
{
using ProcessEventHandler = std::function<void(DWORD)>;
using ProcessEventHandler = void(*)(DWORD);
using ThreadEventHandler = std::function<void(TextThread&)>;
using HookEventHandler = std::function<void(HookParam, std::wstring text)>;
void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output);

View File

@ -1,6 +1,5 @@
#include "textthread.h"
#include "host.h"
#include "util.h"
extern const wchar_t* INVALID_CODEPAGE;
@ -16,14 +15,14 @@ static bool RemoveRepetition(std::wstring& text)
TextThread::TextThread(ThreadParam tp, HookParam hp, std::optional<std::wstring> name) :
handle(threadCounter++),
name(name.value_or(Util::StringToWideString(hp.name).value())),
name(name.value_or(StringToWideString(hp.name))),
tp(tp),
hp(hp)
{}
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()
@ -43,22 +42,34 @@ 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 = Util::StringToWideString(std::string((char*)data, length), hp.codepage ? hp.codepage : Host::defaultCodepage)) buffer.append(converted.value());
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)
{
if (std::all_of(buffer.begin(), buffer.end(), [&](wchar_t ch) { return repeatingChars.find(ch) != repeatingChars.end(); })) buffer.clear();
if (RemoveRepetition(buffer)) // sentence repetition detected, which means the entire sentence has already been received
{
if (hp.type & BLOCK_FLOOD) Host::RemoveHook(tp.processId, tp.addr);
repeatingChars = std::unordered_set(buffer.begin(), buffer.end());
AddSentence(std::move(buffer));
buffer.clear();
@ -72,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()
{
{
@ -79,20 +98,19 @@ void TextThread::Flush()
if (storage->size() > maxHistorySize) storage->erase(0, storage->size() - maxHistorySize); // https://github.com/Artikash/Textractor/issues/127#issuecomment-486882983
}
std::deque<std::wstring> sentences;
std::vector<std::wstring> sentences;
queuedSentences->swap(sentences);
int totalSize = 0;
for (auto& sentence : sentences)
{
totalSize += sentence.size();
sentence.erase(std::remove(sentence.begin(), sentence.end(), L'\0'));
sentence.erase(std::remove(sentence.begin(), sentence.end(), 0), sentence.end());
if (Output(*this, sentence)) storage->append(sentence);
}
if (hp.type & BLOCK_FLOOD && totalSize > PIPE_BUFFER_SIZE) Host::RemoveHook(tp.processId, tp.addr);
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();

View File

@ -1,17 +1,16 @@
#pragma once
#include "common.h"
#include "types.h"
class TextThread
{
public:
using OutputCallback = std::function<bool(TextThread&, std::wstring&)>;
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,8 +36,8 @@ private:
BYTE leadByte = 0;
std::unordered_set<wchar_t> repeatingChars;
std::mutex bufferMutex;
DWORD lastPushTime = 0;
Synchronized<std::deque<std::wstring>> queuedSentences;
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;
};

View File

@ -2,6 +2,7 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <concrt.h>
#include <string>
#include <vector>
#include <deque>
@ -15,8 +16,8 @@
#include <optional>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <atomic>
#include <filesystem>
#include <cstdint>
#include <cassert>
@ -26,22 +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::decay_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;
};
@ -75,24 +69,37 @@ public:
Locker Acquire() { return { std::unique_lock(m), contents }; }
Locker operator->() { return Acquire(); }
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)
@ -113,11 +120,44 @@ 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);
if (int length = MultiByteToWideChar(encoding, 0, text.c_str(), text.size() + 1, buffer.data(), buffer.size()))
return std::wstring(buffer.data(), length - 1);
return {};
}
inline std::wstring StringToWideString(const std::string& text)
{
std::vector<wchar_t> buffer(text.size() + 1);
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size());
return buffer.data();
}
inline std::string WideStringToString(const std::wstring& text)
{
std::vector<char> buffer((text.size() + 1) * 4);
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size(), nullptr, nullptr);
return buffer.data();
}
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

View File

@ -4,7 +4,7 @@
// 8/23/2013 jichi
// Branch: ITH/common.h, rev 128
enum { STRING = 12, MESSAGE_SIZE = 500, PIPE_BUFFER_SIZE = 10000, SHIFT_JIS = 932, MAX_MODULE_SIZE = 120, PATTERN_SIZE = 30, HOOK_NAME_SIZE = 30, FIXED_SPLIT_VALUE = 0x10001 };
enum { STRING = 12, MESSAGE_SIZE = 500, PIPE_BUFFER_SIZE = 50000, SHIFT_JIS = 932, MAX_MODULE_SIZE = 120, PATTERN_SIZE = 30, HOOK_NAME_SIZE = 60, FIXED_SPLIT_VALUE = 0x10001 };
enum WildcardByte { XX = 0x11 };
enum HostCommandType { HOST_COMMAND_NEW_HOOK, HOST_COMMAND_REMOVE_HOOK, HOST_COMMAND_FIND_HOOK, HOST_COMMAND_MODIFY_HOOK, HOST_COMMAND_HIJACK_PROCESS, HOST_COMMAND_DETACH };
@ -27,8 +27,8 @@ enum HookParamType : unsigned
FIXING_SPLIT = 0x800,
DIRECT_READ = 0x1000, // /R read code instead of classic /H hook code
FULL_STRING = 0x2000,
BLOCK_FLOOD = 0x4000, // remove this hook if flooding text causing perf issues
HEX_DUMP = 0x8000,
HOOK_ENGINE = 0x10000,
HOOK_ADDITIONAL = 0x20000,
HEX_DUMP = 0x4000,
HOOK_ENGINE = 0x8000,
HOOK_ADDITIONAL = 0x10000,
KNOWN_UNSTABLE = 0x20000,
};

View File

@ -24,10 +24,6 @@ constexpr auto PIPE_AVAILABLE_EVENT = L"TEXTRACTOR_PIPE_AVAILABLE";
// Files
constexpr auto ITH_DLL = L"texthook"; // .dll but LoadLibrary automatically adds that
constexpr auto CONFIG_FILE = u8"Textractor.ini";
// Misc
constexpr auto WINDOW = u8"Window";
constexpr auto& GAME_CONFIG_FILE = L"TextractorConfig.txt";
// EOF

26
include/module.h Normal file
View File

@ -0,0 +1,26 @@
#include <Psapi.h>
inline std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId))
if (GetModuleFileNameExW(process, module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
inline std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (GetModuleFileNameW(module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
inline std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses()
{
std::vector<DWORD> processIds(10000);
DWORD spaceUsed = 0;
EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed);
std::vector<std::pair<DWORD, std::optional<std::wstring>>> processes;
for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], GetModuleFilename(processIds[i]) });
return processes;
}

View File

@ -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>
@ -22,10 +22,15 @@
static thread_local bool ok;
constexpr auto CONFIG_FILE = u8"Textractor.ini";
constexpr auto WINDOW = u8"Window";
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); }

View File

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

View File

@ -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
@ -24,6 +25,8 @@ Name: "id"; MessagesFile: "compiler:Languages\Unofficial\Indonesian.isl"
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]
@ -37,3 +40,6 @@ Source: "Builds\Textractor-Indonesian-{#VERSION}\*"; DestDir: "{app}"; Languages
Source: "Builds\Textractor-Portuguese-{#VERSION}\*"; DestDir: "{app}"; Languages: pt; Flags: recursesubdirs ignoreversion
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";

View File

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

View File

@ -1,7 +1,4 @@
#include "common.h"
#include "defs.h"
#include "resource.h"
#include <filesystem>
#include "resource.h"
#include <fstream>
#include <sstream>
#include <QApplication>

800
text.cpp

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2244,7 +2244,6 @@ void InsertRealliveHook()
//ConsoleOutput("Probably Reallive. Wait for text.");
ConsoleOutput("vnreng: TRIGGER Reallive");
trigger_fun = InsertRealliveDynamicHook;
SetTrigger();
}
namespace { // unnamed
@ -4495,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;
}
@ -4509,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");
@ -4632,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;
}
@ -5735,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
@ -5804,12 +5799,11 @@ bool InsertShinaHook()
{
int ver = GetShinaRioVersion();
if (ver >= 50) {
SetTrigger();
//trigger_fun = StackSearchingTrigger<GetGlyphOutlineA, NULL>;
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.
@ -5823,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;
}
};
@ -5831,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;
@ -5986,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;
@ -5999,15 +5993,42 @@ 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;
SetTrigger();
if (!found) trigger_fun = InsertWaffleDynamicHook;
return found;
//ConsoleOutput("vnreng:WAFFLE: failed");
}
void InsertTinkerBellHook()
bool InsertTinkerBellHook()
{
//DWORD s1,s2,i;
//DWORD ch=0x8141;
@ -6042,7 +6063,9 @@ void InsertTinkerBellHook()
}
}
}
if (count) return true;
ConsoleOutput("vnreng:TinkerBell: failed");
return false;
}
// s1=SearchPattern(processStartAddress,processStopAddress-processStartAddress-4,&ch,4);
@ -6586,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
@ -8593,7 +8783,6 @@ bool InsertSystemAoiDynamic()
ConsoleOutput("vnreng: DYNAMIC SystemAoi");
//ConsoleOutput("Probably SoftHouseChara. Wait for text.");
trigger_fun = InsertSystemAoiDynamicHook;
SetTrigger();
return true;
}
@ -8880,7 +9069,6 @@ void InsertIronGameSystemHook()
{
//ConsoleOutput("Probably IronGameSystem. Wait for text.");
trigger_fun = InsertIGSDynamicHook;
SetTrigger();
ConsoleOutput("vnreng: TRIGGER IronGameSystem");
}
@ -9412,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;
}
@ -9517,7 +9729,6 @@ void InsertRyokuchaHook()
{
//ConsoleOutput("Probably Ryokucha. Wait for text.");
trigger_fun = InsertRyokuchaDynamicHook;
SetTrigger();
ConsoleOutput("vnreng: TRIGGER Ryokucha");
}
@ -9877,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
{
@ -9916,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;
}
@ -10554,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
@ -13790,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,??
@ -16169,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
@ -16671,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;
}
@ -16717,6 +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");
const BYTE prolog[] = { 0x55, 0x8b, 0xec };
for (auto addr : Util::SearchMemory(prolog, sizeof(prolog), PAGE_EXECUTE_READWRITE))
{
@ -16726,30 +17011,35 @@ void InsertMonoHook(HMODULE h)
{
if (getJitInfo(domain, addr))
if (char* name = getName(addr))
if (strstr(name, "string:") && !strstr(name, "string:mem"))
if (ShouldMonoHook(name))
{
HookParam hp = {};
hp.address = addr;
hp.type = USING_UNICODE | FULL_STRING | BLOCK_FLOOD;
hp.type = USING_UNICODE | FULL_STRING;
if (!*loadedConfig) hp.type |= KNOWN_UNSTABLE;
hp.offset = 4;
char nameForUser[HOOK_NAME_SIZE] = {};
strncpy_s(nameForUser, name + 1, HOOK_NAME_SIZE - 1);
if (char* end = strstr(nameForUser, " + 0x0")) *end = 0;
if (char* end = strstr(nameForUser, "{")) *end = 0;
hp.text_fun = [](DWORD esp_base, HookParam*, BYTE, DWORD* data, DWORD* split, DWORD* len)
{
MonoString* string = (MonoString*)argof(1, esp_base);
*data = (DWORD)string->chars;
*len = string->length * 2;
};
NewHook(hp, name);
NewHook(hp, nameForUser);
}
}
__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");
return true;
failed:
ConsoleOutput("Textractor: Mono Dynamic failed");
return true;
};
SetTrigger();
}
/** jichi 12/26/2014 Mono
@ -16801,10 +17091,11 @@ bool InsertMonoHooks()
if (FARPROC addr = ::GetProcAddress(h, func.functionName)) {
hp.address = (DWORD)addr;
hp.type = func.hookType;
if (*loadedConfig) hp.type |= HOOK_EMPTY;
hp.filter_fun = NoAsciiFilter;
hp.offset = func.textIndex * 4;
hp.length_offset = func.lengthIndex * 4;
hp.text_fun = (decltype(hp.text_fun))func.text_fun;
hp.text_fun = func.text_fun;
ConsoleOutput("vnreng: Mono: INSERT");
NewHook(hp, func.functionName);
ret = true;
@ -20740,6 +21031,14 @@ bool InsertTecmoPSPHook()
*/
bool InsertPCSX2Hooks()
{
memcpy(spDefault.pattern, Array<BYTE>{ 0x89, 0xc8, 0xc1, 0xe8, 0x0c }, spDefault.length = 5);
spDefault.minAddress = 0;
spDefault.maxAddress = -1ULL;
spDefault.offset = 0;
spDefault.searchTime = 60'000;
spDefault.maxRecords = 500'000;
spDefault.padding = 0x20000000;
ConsoleOutput("Textractor: PCSX2 detected (searching for hooks may work)");
// TODO: Add generic hooks
return InsertTypeMoonPS2Hook()
|| InsertMarvelousPS2Hook()

View File

@ -1,8 +1,7 @@
#pragma once
#pragma once
// engine/engine.h
// 8/23/2013 jichi
// See: http://ja.wikipedia.org/wiki/プロジェクト:美少女ゲーム系/ゲームエンジン
#include <windows.h>
@ -15,15 +14,7 @@ namespace Engine {
// Global variables
extern wchar_t *processName, // cached
processPath[MAX_PATH]; // cached
// Artikash 6/17/2019 TODO: These have the wrong values on x64
/** jichi 12/24/2014
* @param addr function address
* @param frame real address of the function, supposed to be the same as addr
* @param stack address of current stack - 4
* @return If success, which is reverted
*/
inline bool (*trigger_fun)(LPVOID addr, DWORD frame, DWORD stack);
inline const char *requestedEngine = "", * loadedConfig = "";
bool InsertMonoHooks(); // Mono
@ -134,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();
@ -171,8 +163,8 @@ void InsertLucifenHook(); // Lucifen@Navel: *.lpk
void InsertRyokuchaHook(); // Ryokucha: _checksum.exe
void InsertRealliveHook(); // RealLive: RealLive*.exe
void InsertStuffScriptHook(); // Stuff: *.mpk
void InsertTinkerBellHook(); // TinkerBell: arc00.dat
void InsertWaffleHook(); // WAFFLE: cg.pak
bool InsertTinkerBellHook(); // TinkerBell: arc00.dat
bool InsertWaffleHook(); // WAFFLE: cg.pak
// CIRCUS: avdata/
bool InsertCircusHook1();

View File

@ -1,6 +1,7 @@
#include "match.h"
#include "engine.h"
#include "main.h"
#include "defs.h"
#include "native/pchooks.h"
extern const char* HIJACK_ERROR;
@ -12,6 +13,8 @@ namespace Engine
WCHAR* processName, // cached
processPath[MAX_PATH]; // cached
char configFileData[1000]{};
bool UnsafeDetermineEngineType();
// jichi 10/21/2014: Return whether found the game engine
@ -30,17 +33,27 @@ namespace Engine
return found;
}
DWORD InsertDynamicHook(LPVOID addr, DWORD frame, DWORD stack)
{
return trigger_fun ? !trigger_fun(addr, frame, stack) : 0;
}
void Hijack()
{
static auto _ = []
static auto _ = ([]
{
GetModuleFileNameW(nullptr, processPath, MAX_PATH);
processName = wcsrchr(processPath, L'\\') + 1;
wchar_t configFilename[MAX_PATH + std::size(GAME_CONFIG_FILE)];
wcsncpy_s(configFilename, processPath, MAX_PATH - 1);
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))
{
ReadFile(configFile, configFileData, sizeof(configFileData) - 1, DUMMY, nullptr);
if (strnicmp(configFileData, "engine:", 7) == 0)
{
if (const char* config = strchr(configFileData, '\n')) *(char*)(loadedConfig = config)++ = 0;
ConsoleOutput("Textractor: Engine = %s", requestedEngine = strlwr(configFileData + 7));
}
else loadedConfig = configFileData;
if (!*loadedConfig || strstr(configFileData, "https://")) loadedConfig = "";
else ConsoleOutput("Textractor: game configuration loaded");
}
processStartAddress = processStopAddress = (uintptr_t)GetModuleHandleW(nullptr);
MEMORY_BASIC_INFORMATION info;
@ -54,8 +67,19 @@ 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");
for (const char* hook = loadedConfig; hook; hook = strchr(hook + 1, '\t'))
for (auto start = name; *start; ++start)
for (int i = 0; ; ++i)
if (start[i] != hook[i + 1]) break;
else if (!hook[i + 2] || hook[i + 2] == '\t') return true;
return false;
}
}

View File

@ -13,10 +13,8 @@ namespace Engine {
// jichi 10/21/2014: Return whether found the engine
void Hijack();
void terminate();
// jichi 10/21/2014: Return 0 if failed
DWORD InsertDynamicHook(LPVOID addr, DWORD frame, DWORD stack);
bool ShouldMonoHook(const char* name);
} // namespace Engine

View File

@ -1,4 +1,4 @@
// match.cc
// match.cc
// 8/9/2013 jichi
// Branch: ITH_Engine/engine.cpp, revision 133
@ -72,6 +72,14 @@ bool DeterminePCEngine()
return true;
}
if (GetModuleHandleW(L"GameAssembly.dll")) // TODO: is there a way to autofind hook?
{
ConsoleOutput("Textractor: Precompiled Unity found (searching for hooks should work)");
wcscpy_s(spDefault.boundaryModule, L"GameAssembly.dll");
spDefault.padding = 12;
return true;
}
// PC games
PcHooks::hookGDIFunctions();
PcHooks::hookGDIPlusFunctions();
@ -273,8 +281,7 @@ bool DetermineEngineByFile2()
InsertWaffleHook();
return true;
}
if (Util::CheckFile(L"Arc00.dat")) {
InsertTinkerBellHook();
if (Util::CheckFile(L"Arc00.dat") && InsertTinkerBellHook()) {
return true;
}
if (Util::CheckFile(L"*.vfs")) { // jichi 7/6/2014: Better to test AoiLib.dll? ja.wikipedia.org/wiki/ソフトハウスキャラ
@ -428,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;
}
@ -606,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;
@ -702,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");

View File

@ -80,6 +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");
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) } })
@ -91,13 +92,18 @@ namespace Engine
{
if (getJitInfo(domain, addr))
if (char* name = getName(addr))
if (strstr(name, "string:") && strstr(name, "+ 0x0") && !strstr(name, "string:mem"))
if (strstr(name, "0x0") && ShouldMonoHook(name))
{
HookParam hp = {};
hp.address = addr;
hp.type = USING_STRING | USING_UNICODE | BLOCK_FLOOD | FULL_STRING;
hp.type = USING_STRING | USING_UNICODE | FULL_STRING;
if (!*loadedConfig) hp.type |= KNOWN_UNSTABLE;
hp.offset = -0x20; // rcx
hp.padding = 20;
char nameForUser[HOOK_NAME_SIZE] = {};
strncpy_s(nameForUser, name + 1, HOOK_NAME_SIZE - 1);
if (char* end = strstr(nameForUser, " + 0x0")) *end = 0;
if (char* end = strstr(nameForUser, "{")) *end = 0;
hp.length_fun = [](uintptr_t, uintptr_t data)
{
/* Artikash 6/18/2019:
@ -106,18 +112,19 @@ namespace Engine
int len = *(int*)(data - 4);
return len > 0 && len < PIPE_BUFFER_SIZE ? len * 2 : 0;
};
NewHook(hp, name);
NewHook(hp, nameForUser);
}
}
__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");
return true;
failed:
ConsoleOutput("Textractor: Mono Dynamic failed");
return true;
};
SetTrigger();
return ret;
}
@ -125,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;
}
@ -153,6 +220,16 @@ namespace Engine
for (const wchar_t* moduleName : { (const wchar_t*)NULL, L"node.dll", L"nw.dll" }) if (InsertV8Hook(GetModuleHandleW(moduleName))) return true;
if (GetModuleHandleW(L"GameAssembly.dll")) // TODO: is there a way to autofind hook?
{
ConsoleOutput("Textractor: Precompiled Unity found (searching for hooks should work)");
wcscpy_s(spDefault.boundaryModule, L"GameAssembly.dll");
spDefault.padding = 20;
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" })

View File

@ -38,7 +38,7 @@ struct MonoFunction { // argument indices start from 0 for SpecialHookMonoString
size_t textIndex; // argument index
short lengthIndex; // argument index
unsigned long hookType; // HookParam type
void *text_fun; // HookParam::text_fun_t
void(*text_fun)(DWORD stack, HookParam* hp, BYTE obsoleteAlwaysZero, DWORD* data, DWORD* split, DWORD* len); // HookParam::text_fun_t
};
#define MONO_FUNCTIONS_INITIALIZER \

View File

@ -1,4 +1,4 @@
// pchooks.cc
// pchooks.cc
// 8/1/2014 jichi
#include "pchooks.h"
@ -279,11 +279,18 @@ void PcHooks::hookOtherPcFunctions()
// _Out_opt_ LPBOOL lpUsedDefaultChar
// );
// 3/17/2014 jichi: Temporarily disabled
// 2/29/2020 Artikash: TODO: Sort out what to do for string comparison functions
// http://sakuradite.com/topic/159
NEW_HOOK(L"kernel32.dll", MultiByteToWideChar, s_arg3, 0,4,0, USING_STRING, s_arg4 / arg_sz)
NEW_HOOK(L"kernel32.dll", WideCharToMultiByte, s_arg3, 0,4,0, USING_UNICODE|USING_STRING, s_arg4 / arg_sz)
NEW_HOOK(L"kernel32.dll", GetStringTypeA, s_arg3, 0, 0, 0, USING_STRING, s_arg4 / arg_sz)
NEW_HOOK(L"kernel32.dll", GetStringTypeExA, s_arg3, 0, 0, 0, USING_STRING, s_arg4 / arg_sz)
NEW_HOOK(L"kernel32.dll", FoldStringA, s_arg2, 0, 0, 0, USING_STRING, s_arg3 / arg_sz)
NEW_HOOK(L"kernel32.dll", GetStringTypeW, s_arg2, 0, 0, 0, USING_UNICODE|USING_STRING, s_arg3 / arg_sz)
NEW_HOOK(L"kernel32.dll", GetStringTypeExW, s_arg3, 0, 0, 0, USING_UNICODE|USING_STRING, s_arg4 / arg_sz)
NEW_HOOK(L"kernel32.dll", FoldStringW, s_arg2, 0, 0, 0, USING_UNICODE|USING_STRING, s_arg3 / arg_sz)
NEW_HOOK(L"user32.dll", CharNextA, s_arg1, 0,0,0, USING_STRING|DATA_INDIRECT, 1) // LPTSTR WINAPI CharNext(_In_ LPCTSTR lpsz);
NEW_HOOK(L"user32.dll", CharNextW, s_arg1, 0,0,0, USING_UNICODE|DATA_INDIRECT, 1)
NEW_HOOK(L"user32.dll", CharPrevA, s_arg1, 0,0,0, USING_STRING|DATA_INDIRECT, 1) // LPTSTR WINAPI CharPrev(_In_ LPCTSTR lpszStart, _In_ LPCTSTR lpszCurrent);
@ -293,7 +300,7 @@ void PcHooks::hookOtherPcFunctions()
if (HMODULE module = GetModuleHandleW(L"OLEAUT32.dll"))
{
NEW_MODULE_HOOK(module, SysAllocString, s_arg1, 0, 0, 0, USING_UNICODE|USING_STRING, 0)
NEW_MODULE_HOOK(module, SysAllocStringLen, s_arg1, 0, 0, 0, USING_UNICODE|USING_STRING, s_arg2 / arg_sz)
NEW_MODULE_HOOK(module, SysAllocStringLen, s_arg1, 0, 0, 0, USING_UNICODE|USING_STRING|KNOWN_UNSTABLE, s_arg2 / arg_sz)
}
}

View File

@ -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;
@ -19,6 +21,7 @@ namespace
struct HookRecord
{
uint64_t address = 0;
uintptr_t padding = 0;
int offset = 0;
char text[MAX_STRING_SIZE] = {};
};
@ -67,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
@ -76,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
@ -96,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;
@ -134,9 +137,9 @@ void Send(char** stack, uintptr_t address)
// it is unsafe to call ANY external functions from this, as they may have been hooked (if called the hook would call this function making an infinite loop)
// the exceptions are compiler intrinsics like _InterlockedDecrement
if (recordsAvailable <= 0) return;
for (int i = -registers; i < 10; ++i)
for (int i = -registers; i < 10; ++i) for (auto padding : { uintptr_t{}, sp.padding })
{
char* str = stack[i] + sp.padding;
char* str = stack[i] + padding;
if (IsBadReadPtr(str) || IsBadReadPtr(str + MAX_STRING_SIZE)) continue;
__try
{
@ -155,6 +158,7 @@ void Send(char** stack, uintptr_t address)
if (n < sp.maxRecords)
{
records[n].address = address;
records[n].padding = padding;
records[n].offset = i * sizeof(char*);
for (int j = 0; j < length; ++j) records[n].text[j] = str[j];
records[n].text[length] = 0;
@ -198,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));
@ -220,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);
@ -240,7 +246,7 @@ void SearchForHooks(SearchParam spUser)
hp.offset = records[i].offset;
hp.type = USING_UNICODE | USING_STRING;
hp.address = records[i].address;
hp.padding = sp.padding;
hp.padding = records[i].padding;
hp.codepage = sp.codepage;
if (sp.hookPostProcessor) sp.hookPostProcessor(hp);
NotifyHookFound(hp, (wchar_t*)records[i].text);
@ -259,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)

View File

@ -1,6 +1,5 @@
#pragma once
#include "common.h"
#include "types.h"
void SearchForText(wchar_t* text, UINT codepage);

View File

@ -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;
@ -86,13 +87,10 @@ DWORD WINAPI Pipe(LPVOID)
FreeLibraryAndExitThread(GetModuleHandleW(ITH_DLL), 0);
}
void TextOutput(ThreadParam tp, BYTE* text, int len)
void TextOutput(ThreadParam tp, BYTE (*buffer)[PIPE_BUFFER_SIZE], int len)
{
if (len < 0) return;
if (len > PIPE_BUFFER_SIZE - sizeof(tp)) len = PIPE_BUFFER_SIZE - sizeof(tp);
BYTE buffer[PIPE_BUFFER_SIZE] = {};
if (len < 0 || len > PIPE_BUFFER_SIZE - sizeof(tp)) ConsoleOutput("Textractor: something went very wrong (invalid length %d at hook address %I64d)", len, tp.addr);
*(ThreadParam*)buffer = tp;
memcpy(buffer + sizeof(tp), text, len);
WriteFile(hookPipe, buffer, sizeof(tp) + len, DUMMY, nullptr);
}

View File

@ -4,10 +4,9 @@
// 8/23/2013 jichi
// Branch: ITH/IHF_DLL.h, rev 66
#include "common.h"
#include "types.h"
void TextOutput(ThreadParam tp, BYTE* text, int len);
void TextOutput(ThreadParam tp, BYTE (*buffer)[PIPE_BUFFER_SIZE], int len);
void ConsoleOutput(LPCSTR text, ...);
void NotifyHookFound(HookParam hp, wchar_t* text);
void NotifyHookRemove(uint64_t addr, LPCSTR name);
@ -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

@ -0,0 +1 @@
Subproject commit 423d1e45af2ed2719a5c31e990e935ef301ed9c3

View File

@ -5,8 +5,8 @@
#include "texthook.h"
#include "main.h"
#include "engine/match.h"
#include "ithsys/ithsys.h"
#include "MinHook.h"
extern const char* FUNC_MISSING;
extern const char* MODULE_MISSING;
@ -31,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
};
@ -58,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
@ -67,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
@ -87,43 +87,41 @@ 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;
#endif
bool trigger = false;
thread_local BYTE buffer[PIPE_BUFFER_SIZE];
enum { TEXT_BUFFER_SIZE = PIPE_BUFFER_SIZE - sizeof(ThreadParam) };
} // unnamed namespace
void SetTrigger()
{
trigger = true;
}
// - TextHook methods -
bool TextHook::Insert(HookParam hp, DWORD set_flag)
{
std::scoped_lock lock(viewMutex);
hp.type |= set_flag;
if (hp.type & USING_UTF8) hp.codepage = CP_UTF8;
this->hp = hp;
address = hp.address;
{
std::scoped_lock lock(viewMutex);
hp.type |= set_flag;
if (hp.type & USING_UTF8) hp.codepage = CP_UTF8;
this->hp = hp;
address = hp.address;
}
if (hp.type & DIRECT_READ) return InsertReadCode();
else return InsertHookCode();
return InsertHookCode();
}
// jichi 5/11/2014:
// - dwDataBase: the stack address
void TextHook::Send(uintptr_t dwDataBase)
{
BYTE(*buffer)[PIPE_BUFFER_SIZE] = &::buffer, *pbData = *buffer + sizeof(ThreadParam);
_InterlockedIncrement(&useCount);
__try
{
if (trigger) trigger = Engine::InsertDynamicHook(location, *(DWORD *)(dwDataBase - 0x1c), *(DWORD *)(dwDataBase - 0x18));
if (auto current_trigger_fun = trigger_fun.exchange(nullptr))
if (!current_trigger_fun(location, *(DWORD*)(dwDataBase - 0x1c), *(DWORD*)(dwDataBase - 0x18))) trigger_fun = current_trigger_fun;
#ifndef _WIN64
DWORD dwCount = 0,
@ -131,7 +129,6 @@ void TextHook::Send(uintptr_t dwDataBase)
dwDataIn = *(DWORD*)(dwDataBase + hp.offset), // default values
dwRetn = *(DWORD*)dwDataBase; // first value on stack (if hooked start of function, this is return address)
// jichi 10/24/2014: generic hook function
if (hp.hook_fun && !hp.hook_fun(dwDataBase, &hp)) hp.hook_fun = nullptr;
@ -151,9 +148,8 @@ void TextHook::Send(uintptr_t dwDataBase)
dwCount = GetLength(dwDataBase, dwDataIn);
}
if (dwCount == 0) goto done;
if (dwCount <= 0) goto done;
if (dwCount > TEXT_BUFFER_SIZE) dwCount = TEXT_BUFFER_SIZE;
BYTE pbData[TEXT_BUFFER_SIZE];
if (hp.length_offset == 1) {
dwDataIn &= 0xffff;
if ((hp.type & BIG_ENDIAN) && (dwDataIn >> 8)) dwDataIn = _byteswap_ushort(dwDataIn & 0xffff);
@ -166,7 +162,7 @@ void TextHook::Send(uintptr_t dwDataBase)
if (hp.type & (NO_CONTEXT | FIXING_SPLIT)) dwRetn = 0;
TextOutput({ GetCurrentProcessId(), address, dwRetn, dwSplit }, pbData, dwCount);
TextOutput({ GetCurrentProcessId(), address, dwRetn, dwSplit }, buffer, dwCount);
#else // _WIN32
if (hp.type & HOOK_EMPTY) goto done; // jichi 10/24/2014: dummy hook only for dynamic hook
int count = 0;
@ -182,9 +178,8 @@ void TextHook::Send(uintptr_t dwDataBase)
data += hp.padding;
count = GetLength(dwDataBase, data);
if (count == 0) goto done;
if (count <= 0) goto done;
if (count > TEXT_BUFFER_SIZE) count = TEXT_BUFFER_SIZE;
BYTE pbData[TEXT_BUFFER_SIZE];
if (hp.length_offset == 1)
{
data &= 0xffff;
@ -196,12 +191,13 @@ void TextHook::Send(uintptr_t dwDataBase)
if (hp.type & (NO_CONTEXT | FIXING_SPLIT)) tp.ctx = 0;
TextOutput(tp, pbData, count);
TextOutput(tp, buffer, count);
#endif // _WIN64
++*pbData;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
if (!err)
if (!err && !(hp.type & KNOWN_UNSTABLE))
{
err = true;
ConsoleOutput("%s in %s", SEND_ERROR, hp.name);
@ -222,6 +218,7 @@ bool TextHook::InsertHookCode()
else if (HMODULE moduleBase = GetModuleHandleW(hp.module)) address += (uint64_t)moduleBase;
else return ConsoleOutput(MODULE_MISSING), false;
VirtualProtect(location, 10, PAGE_EXECUTE_READWRITE, DUMMY);
void* original;
MH_STATUS error;
while ((error = MH_CreateHook(location, trampoline, &original)) != MH_OK)
@ -237,14 +234,14 @@ bool TextHook::InsertHookCode()
void TextHook::Read()
{
BYTE buffer[TEXT_BUFFER_SIZE] = {};
int dataLen = 1;
BYTE(*buffer)[PIPE_BUFFER_SIZE] = &::buffer, *pbData = *buffer + sizeof(ThreadParam);
__try
{
while (WaitForSingleObject(readerEvent, 500) == WAIT_TIMEOUT) if (memcmp(buffer, location, dataLen) != 0) if (int currentLen = HookStrlen((BYTE*)location))
while (WaitForSingleObject(readerEvent, 500) == WAIT_TIMEOUT) if (memcmp(pbData, location, dataLen) != 0) if (int currentLen = HookStrlen((BYTE*)location))
{
dataLen = min(currentLen, TEXT_BUFFER_SIZE);
memcpy(buffer, location, dataLen);
memcpy(pbData, location, dataLen);
TextOutput({ GetCurrentProcessId(), address, 0, 0 }, buffer, dataLen);
}
}
@ -312,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;
}

View File

@ -7,10 +7,16 @@
// 8/24/2013 TODO:
// - Clean up this file
// - Reduce global variables. Use namespaces or singleton classes instead.
#include "common.h"
#include "types.h"
void SetTrigger();
// Artikash 6/17/2019 TODO: These have the wrong values on x64
/** jichi 12/24/2014
* @param addr function address
* @param frame real address of the function, supposed to be the same as addr
* @param stack address of current stack - 4
* @return If success, which is reverted
*/
inline std::atomic<bool (*)(LPVOID addr, DWORD frame, DWORD stack)> trigger_fun = nullptr;
// jichi 9/25/2013: This class will be used by NtMapViewOfSectionfor
// interprocedure communication, where constructor/destructor will NOT work.
@ -45,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

Some files were not shown because too many files have changed in this diff Show More